From 42c4bf6c5d4d34659fd808414495f428cd3a9e1e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 01:37:40 +0000 Subject: [PATCH 1/7] build(deps): bump socket2 from 0.6.2 to 0.6.3 (#3966) Bumps [socket2](https://github.com/rust-lang/socket2) from 0.6.2 to 0.6.3. - [Release notes](https://github.com/rust-lang/socket2/releases) - [Changelog](https://github.com/rust-lang/socket2/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/socket2/compare/v0.6.2...v0.6.3) --- updated-dependencies: - dependency-name: socket2 dependency-version: 0.6.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f4088f0b2..c7d73d1ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,7 +126,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "slab", - "socket2 0.6.2", + "socket2 0.6.3", "tokio", ] @@ -372,7 +372,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2 0.6.2", + "socket2 0.6.3", "static_assertions", "time", "tokio", @@ -2769,12 +2769,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2997,7 +2997,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.2", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] From 8f6d300b64a307bc26acefffca26f6944ff486a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 01:37:44 +0000 Subject: [PATCH 2/7] build(deps): bump taiki-e/install-action from 2.68.15 to 2.68.25 (#3963) Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.68.15 to 2.68.25. - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/68675c5a5f1a6950c3975d33f3ae0ef155e5bf3d...a37010ded18ff788be4440302bd6830b1ae50d8b) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.68.25 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-post-merge.yml | 4 ++-- .github/workflows/ci.yml | 4 ++-- .github/workflows/coverage.yml | 2 +- .github/workflows/lint.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-post-merge.yml b/.github/workflows/ci-post-merge.yml index afac60f83..4da2a3860 100644 --- a/.github/workflows/ci-post-merge.yml +++ b/.github/workflows/ci-post-merge.yml @@ -49,7 +49,7 @@ jobs: toolchain: ${{ matrix.version.version }} - name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean - uses: taiki-e/install-action@68675c5a5f1a6950c3975d33f3ae0ef155e5bf3d # v2.68.15 + uses: taiki-e/install-action@a37010ded18ff788be4440302bd6830b1ae50d8b # v2.68.25 with: tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean @@ -83,7 +83,7 @@ jobs: uses: actions-rust-lang/setup-rust-toolchain@a0b538fa0b742a6aa35d6e2c169b4bd06d225a98 # v1.15.3 - name: Install just, cargo-hack - uses: taiki-e/install-action@68675c5a5f1a6950c3975d33f3ae0ef155e5bf3d # v2.68.15 + uses: taiki-e/install-action@a37010ded18ff788be4440302bd6830b1ae50d8b # v2.68.25 with: tool: just,cargo-hack diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b77887e66..1883cc170 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,7 +64,7 @@ jobs: toolchain: ${{ matrix.version.version }} - name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean - uses: taiki-e/install-action@68675c5a5f1a6950c3975d33f3ae0ef155e5bf3d # v2.68.15 + uses: taiki-e/install-action@a37010ded18ff788be4440302bd6830b1ae50d8b # v2.68.25 with: tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean @@ -117,7 +117,7 @@ jobs: toolchain: nightly - name: Install just - uses: taiki-e/install-action@68675c5a5f1a6950c3975d33f3ae0ef155e5bf3d # v2.68.15 + uses: taiki-e/install-action@a37010ded18ff788be4440302bd6830b1ae50d8b # v2.68.25 with: tool: just diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 46cf1d863..d2f00e4bf 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -24,7 +24,7 @@ jobs: components: llvm-tools - name: Install just, cargo-llvm-cov, cargo-nextest - uses: taiki-e/install-action@68675c5a5f1a6950c3975d33f3ae0ef155e5bf3d # v2.68.15 + uses: taiki-e/install-action@a37010ded18ff788be4440302bd6830b1ae50d8b # v2.68.25 with: tool: just,cargo-llvm-cov,cargo-nextest diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 814d6e823..196d4a321 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -77,7 +77,7 @@ jobs: toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }} - name: Install just - uses: taiki-e/install-action@68675c5a5f1a6950c3975d33f3ae0ef155e5bf3d # v2.68.15 + uses: taiki-e/install-action@a37010ded18ff788be4440302bd6830b1ae50d8b # v2.68.25 with: tool: just From 51f43ae4c522450697b48329615353480b98bbf7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 01:37:49 +0000 Subject: [PATCH 3/7] build(deps): bump tokio from 1.49.0 to 1.50.0 (#3965) Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.49.0 to 1.50.0. - [Release notes](https://github.com/tokio-rs/tokio/releases) - [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.49.0...tokio-1.50.0) --- updated-dependencies: - dependency-name: tokio dependency-version: 1.50.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c7d73d1ec..dee14543d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2987,9 +2987,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", From 5d1b742d0f68e24bc1eedeb8e539437b171b3e6f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 01:37:53 +0000 Subject: [PATCH 4/7] build(deps): bump taiki-e/cache-cargo-install-action from 3.0.2 to 3.0.3 (#3962) Bumps [taiki-e/cache-cargo-install-action](https://github.com/taiki-e/cache-cargo-install-action) from 3.0.2 to 3.0.3. - [Release notes](https://github.com/taiki-e/cache-cargo-install-action/releases) - [Changelog](https://github.com/taiki-e/cache-cargo-install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/cache-cargo-install-action/compare/2bfc3cedaf2ee5e7fa5d0ae034ccd5fb50cf8e1f...59027ebf20a9617c4e819eb53ccd2673cb162b89) --- updated-dependencies: - dependency-name: taiki-e/cache-cargo-install-action dependency-version: 3.0.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 196d4a321..cabef715a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -82,7 +82,7 @@ jobs: tool: just - name: Install cargo-check-external-types - uses: taiki-e/cache-cargo-install-action@2bfc3cedaf2ee5e7fa5d0ae034ccd5fb50cf8e1f # v3.0.2 + uses: taiki-e/cache-cargo-install-action@59027ebf20a9617c4e819eb53ccd2673cb162b89 # v3.0.3 with: tool: cargo-check-external-types From 245b511dfd12edb98c9cabd9ecc4d13b519c3ace Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 01:37:58 +0000 Subject: [PATCH 5/7] build(deps): bump quote from 1.0.44 to 1.0.45 (#3964) Bumps [quote](https://github.com/dtolnay/quote) from 1.0.44 to 1.0.45. - [Release notes](https://github.com/dtolnay/quote/releases) - [Commits](https://github.com/dtolnay/quote/compare/1.0.44...1.0.45) --- updated-dependencies: - dependency-name: quote dependency-version: 1.0.45 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dee14543d..9df8ea033 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2215,9 +2215,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] From 79434bde724f85d45c3d161a5ce603260043779a Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 10 Mar 2026 10:16:35 +0000 Subject: [PATCH 6/7] fix: correct HeaderMap iterators' len and size_hint for multi-value (#3971) * test: add headermap iterator tests confirmed new tests fail as expected * fix: correct HeaderMap iterators' len and size_hint for multi-value headers fixes #3969 --- actix-http/CHANGES.md | 1 + actix-http/src/header/map.rs | 71 ++++++++++++++++++++++++++++-------- 2 files changed, 56 insertions(+), 16 deletions(-) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 9edd7b6f0..a627718b0 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -3,6 +3,7 @@ ## Unreleased - Encode the HTTP/1 `Connection: Upgrade` header in Camel-Case when camel-case header formatting is enabled.[#3953] +- Fix `HeaderMap` iterators' `len()` and `size_hint()` implementations for multi-value headers. [#3953]: https://github.com/actix/actix-web/pull/3953 diff --git a/actix-http/src/header/map.rs b/actix-http/src/header/map.rs index a9a201e1a..69f0aa2a7 100644 --- a/actix-http/src/header/map.rs +++ b/actix-http/src/header/map.rs @@ -537,7 +537,7 @@ impl HeaderMap { /// assert!(pairs.contains(&(&header::SET_COOKIE, &HeaderValue::from_static("two=2")))); /// ``` pub fn iter(&self) -> Iter<'_> { - Iter::new(self.inner.iter()) + Iter::new(self.inner.iter(), self.len()) } /// An iterator over all contained header names. @@ -626,7 +626,8 @@ impl HeaderMap { /// assert!(map.is_empty()); /// ``` pub fn drain(&mut self) -> Drain<'_> { - Drain::new(self.inner.drain()) + let len = self.len(); + Drain::new(self.inner.drain(), len) } } @@ -638,7 +639,8 @@ impl IntoIterator for HeaderMap { #[inline] fn into_iter(self) -> Self::IntoIter { - IntoIter::new(self.inner.into_iter()) + let len = self.len(); + IntoIter::new(self.inner.into_iter(), len) } } @@ -648,7 +650,7 @@ impl<'a> IntoIterator for &'a HeaderMap { #[inline] fn into_iter(self) -> Self::IntoIter { - Iter::new(self.inner.iter()) + Iter::new(self.inner.iter(), self.len()) } } @@ -760,14 +762,16 @@ pub struct Iter<'a> { inner: hash_map::Iter<'a, HeaderName, Value>, multi_inner: Option<(&'a HeaderName, &'a SmallVec<[HeaderValue; 4]>)>, multi_idx: usize, + remaining: usize, } impl<'a> Iter<'a> { - fn new(iter: hash_map::Iter<'a, HeaderName, Value>) -> Self { + fn new(iter: hash_map::Iter<'a, HeaderName, Value>, remaining: usize) -> Self { Self { inner: iter, multi_idx: 0, multi_inner: None, + remaining, } } } @@ -781,6 +785,7 @@ impl<'a> Iterator for Iter<'a> { match vals.get(self.multi_idx) { Some(val) => { self.multi_idx += 1; + self.remaining -= 1; return Some((name, val)); } None => { @@ -800,9 +805,7 @@ impl<'a> Iterator for Iter<'a> { #[inline] fn size_hint(&self) -> (usize, Option) { - // take inner lower bound - // make no attempt at an upper bound - (self.inner.size_hint().0, None) + (self.remaining, Some(self.remaining)) } } @@ -818,14 +821,16 @@ pub struct Drain<'a> { inner: hash_map::Drain<'a, HeaderName, Value>, multi_inner: Option<(Option, SmallVec<[HeaderValue; 4]>)>, multi_idx: usize, + remaining: usize, } impl<'a> Drain<'a> { - fn new(iter: hash_map::Drain<'a, HeaderName, Value>) -> Self { + fn new(iter: hash_map::Drain<'a, HeaderName, Value>, remaining: usize) -> Self { Self { inner: iter, multi_inner: None, multi_idx: 0, + remaining, } } } @@ -838,6 +843,7 @@ impl Iterator for Drain<'_> { if let Some((ref mut name, ref mut vals)) = self.multi_inner { if !vals.is_empty() { // OPTIMIZE: array removals + self.remaining -= 1; return Some((name.take(), vals.remove(0))); } else { // no more items in value iterator; reset state @@ -855,9 +861,7 @@ impl Iterator for Drain<'_> { #[inline] fn size_hint(&self) -> (usize, Option) { - // take inner lower bound - // make no attempt at an upper bound - (self.inner.size_hint().0, None) + (self.remaining, Some(self.remaining)) } } @@ -872,13 +876,15 @@ impl iter::FusedIterator for Drain<'_> {} pub struct IntoIter { inner: hash_map::IntoIter, multi_inner: Option<(HeaderName, smallvec::IntoIter<[HeaderValue; 4]>)>, + remaining: usize, } impl IntoIter { - fn new(inner: hash_map::IntoIter) -> Self { + fn new(inner: hash_map::IntoIter, remaining: usize) -> Self { Self { inner, multi_inner: None, + remaining, } } } @@ -891,6 +897,7 @@ impl Iterator for IntoIter { if let Some((ref name, ref mut vals)) = self.multi_inner { match vals.next() { Some(val) => { + self.remaining -= 1; return Some((name.clone(), val)); } None => { @@ -909,9 +916,7 @@ impl Iterator for IntoIter { #[inline] fn size_hint(&self) -> (usize, Option) { - // take inner lower bound - // make no attempt at an upper bound - (self.inner.size_hint().0, None) + (self.remaining, Some(self.remaining)) } } @@ -1160,6 +1165,40 @@ mod tests { assert!(vals.next().is_none()); } + #[test] + fn iter_len_counts_values() { + let mut map = HeaderMap::new(); + map.append(header::SET_COOKIE, HeaderValue::from_static("a=1")); + map.append(header::SET_COOKIE, HeaderValue::from_static("b=2")); + map.append(header::SET_COOKIE, HeaderValue::from_static("c=3")); + + assert_eq!(map.iter().count(), 3); + assert_eq!(map.iter().len(), 3); + } + + #[test] + fn into_iter_len_counts_values() { + let mut map = HeaderMap::new(); + map.append(header::SET_COOKIE, HeaderValue::from_static("a=1")); + map.append(header::SET_COOKIE, HeaderValue::from_static("b=2")); + map.append(header::SET_COOKIE, HeaderValue::from_static("c=3")); + + assert_eq!(map.clone().into_iter().count(), 3); + assert_eq!(map.into_iter().len(), 3); + } + + #[test] + fn drain_len_counts_values() { + let mut map = HeaderMap::new(); + map.append(header::SET_COOKIE, HeaderValue::from_static("a=1")); + map.append(header::SET_COOKIE, HeaderValue::from_static("b=2")); + map.append(header::SET_COOKIE, HeaderValue::from_static("c=3")); + + let mut drained = map.clone(); + assert_eq!(map.drain().count(), 3); + assert_eq!(drained.drain().len(), 3); + } + fn owned_pair<'a>((name, val): (&'a HeaderName, &'a HeaderValue)) -> (HeaderName, HeaderValue) { (name.clone(), val.clone()) } From 9c2864c2c36bb7fc361b7ac8a120677d7bd98e3f Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 10 Mar 2026 10:28:26 +0000 Subject: [PATCH 7/7] fix: yield lines correctly in readlines across split chunks (#3970) --- actix-web/CHANGES.md | 1 + actix-web/src/types/readlines.rs | 116 ++++++++++++++++++++----------- 2 files changed, 78 insertions(+), 39 deletions(-) diff --git a/actix-web/CHANGES.md b/actix-web/CHANGES.md index ba53e912a..59901dfd4 100644 --- a/actix-web/CHANGES.md +++ b/actix-web/CHANGES.md @@ -4,6 +4,7 @@ - Panic when calling `Route::to()` or `Route::service()` after `Route::wrap()` to prevent silently dropping route middleware. [#3944] - Fix `HttpRequest::{match_pattern,match_name}` reporting path-only matches when route guards disambiguate overlapping resources. [#3346] +- Fix `Readlines` handling of lines split across payload chunks so combined line limits are enforced and complete lines are yielded. [#3944]: https://github.com/actix/actix-web/pull/3944 [#3346]: https://github.com/actix/actix-web/issues/3346 diff --git a/actix-web/src/types/readlines.rs b/actix-web/src/types/readlines.rs index e75239968..17998a1a3 100644 --- a/actix-web/src/types/readlines.rs +++ b/actix-web/src/types/readlines.rs @@ -65,6 +65,23 @@ where err: Some(err), } } + + /// Decodes one complete logical line using the request's configured encoding. + /// + /// Callers are expected to pass only the bytes that belong to the line being yielded, + /// whether they came from the internal buffer, the current payload chunk, or both. + fn decode(encoding: &'static Encoding, bytes: &[u8]) -> Result { + if encoding == UTF_8 { + str::from_utf8(bytes) + .map_err(|_| ReadlinesError::EncodingError) + .map(str::to_owned) + } else { + encoding + .decode_without_bom_handling_and_without_replacement(bytes) + .map(Cow::into_owned) + .ok_or(ReadlinesError::EncodingError) + } + } } impl Stream for Readlines @@ -95,18 +112,7 @@ where if ind + 1 > this.limit { return Poll::Ready(Some(Err(ReadlinesError::LimitOverflow))); } - let line = if this.encoding == UTF_8 { - str::from_utf8(&this.buf.split_to(ind + 1)) - .map_err(|_| ReadlinesError::EncodingError)? - .to_owned() - } else { - this.encoding - .decode_without_bom_handling_and_without_replacement( - &this.buf.split_to(ind + 1), - ) - .map(Cow::into_owned) - .ok_or(ReadlinesError::EncodingError)? - }; + let line = Self::decode(this.encoding, &this.buf.split_to(ind + 1))?; return Poll::Ready(Some(Ok(line))); } this.checked_buff = true; @@ -125,24 +131,17 @@ where } if let Some(ind) = found { // check if line is longer than limit - if ind + 1 > this.limit { + if this.buf.len() + ind + 1 > this.limit { return Poll::Ready(Some(Err(ReadlinesError::LimitOverflow))); } - let line = if this.encoding == UTF_8 { - str::from_utf8(&bytes.split_to(ind + 1)) - .map_err(|_| ReadlinesError::EncodingError)? - .to_owned() - } else { - this.encoding - .decode_without_bom_handling_and_without_replacement( - &bytes.split_to(ind + 1), - ) - .map(Cow::into_owned) - .ok_or(ReadlinesError::EncodingError)? - }; - // extend buffer with rest of the bytes; + + this.buf.extend_from_slice(&bytes.split_to(ind + 1)); + let line = Self::decode(this.encoding, &this.buf)?; + this.buf.clear(); + + // buffer bytes following the returned line this.buf.extend_from_slice(&bytes); - this.checked_buff = false; + this.checked_buff = this.buf.is_empty(); return Poll::Ready(Some(Ok(line))); } this.buf.extend_from_slice(&bytes); @@ -156,16 +155,7 @@ where if this.buf.len() > this.limit { return Poll::Ready(Some(Err(ReadlinesError::LimitOverflow))); } - let line = if this.encoding == UTF_8 { - str::from_utf8(&this.buf) - .map_err(|_| ReadlinesError::EncodingError)? - .to_owned() - } else { - this.encoding - .decode_without_bom_handling_and_without_replacement(&this.buf) - .map(Cow::into_owned) - .ok_or(ReadlinesError::EncodingError)? - }; + let line = Self::decode(this.encoding, &this.buf)?; this.buf.clear(); Poll::Ready(Some(Ok(line))) } @@ -177,10 +167,16 @@ where #[cfg(test)] mod tests { - use futures_util::StreamExt as _; + use std::{ + pin::Pin, + task::{Context, Poll}, + }; + + use actix_http::{h1, Request}; + use futures_util::{task::noop_waker_ref, StreamExt as _}; use super::*; - use crate::test::TestRequest; + use crate::{error::ReadlinesError, test::TestRequest}; #[actix_rt::test] async fn test_readlines() { @@ -208,4 +204,46 @@ mod tests { "Contrary to popular belief, Lorem Ipsum is not simply random text." ); } + + #[test] + fn test_readlines_limit_across_chunks() { + let (mut sender, payload) = h1::Payload::create(false); + let payload: actix_http::Payload = payload.into(); + let mut req = Request::with_payload(payload); + let mut stream = Readlines::new(&mut req).limit(10); + let mut cx = Context::from_waker(noop_waker_ref()); + + sender.feed_data(Bytes::from_static(b"AAAAAAAAAA")); + assert!(matches!( + Pin::new(&mut stream).poll_next(&mut cx), + Poll::Pending + )); + + sender.feed_data(Bytes::from_static(b"A\n")); + assert!(matches!( + Pin::new(&mut stream).poll_next(&mut cx), + Poll::Ready(Some(Err(ReadlinesError::LimitOverflow))) + )); + } + + #[test] + fn test_readlines_returns_full_line_across_chunks() { + let (mut sender, payload) = h1::Payload::create(false); + let payload: actix_http::Payload = payload.into(); + let mut req = Request::with_payload(payload); + let mut stream = Readlines::new(&mut req); + let mut cx = Context::from_waker(noop_waker_ref()); + + sender.feed_data(Bytes::from_static(b"hello ")); + assert!(matches!( + Pin::new(&mut stream).poll_next(&mut cx), + Poll::Pending + )); + + sender.feed_data(Bytes::from_static(b"world\nnext")); + assert!(matches!( + Pin::new(&mut stream).poll_next(&mut cx), + Poll::Ready(Some(Ok(ref line))) if line == "hello world\n" + )); + } }