diff --git a/.github/workflows/ci-post-merge.yml b/.github/workflows/ci-post-merge.yml index d37b2c107..9fce98f4c 100644 --- a/.github/workflows/ci-post-merge.yml +++ b/.github/workflows/ci-post-merge.yml @@ -78,15 +78,6 @@ jobs: cargo test --lib --tests -p=actix-multipart --all-features cargo test --lib --tests -p=actix-web-actors --all-features - - name: tests (io-uring) - if: matrix.target.os == 'ubuntu-latest' - timeout-minutes: 60 - run: > - sudo bash -c "ulimit -Sl 512 - && ulimit -Hl 512 - && PATH=$PATH:/usr/share/rust/.cargo/bin - && RUSTUP_TOOLCHAIN=${{ matrix.version }} cargo test --lib --tests -p=actix-files --all-features" - - name: Clear the cargo caches run: | cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean @@ -125,34 +116,6 @@ jobs: uses: actions-rs/cargo@v1 with: { command: ci-check-all-feature-powerset-linux } - # job currently (1st Feb 2022) segfaults - # coverage: - # name: coverage - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v2 - - # - name: Install stable - # uses: actions-rs/toolchain@v1 - # with: - # toolchain: stable-x86_64-unknown-linux-gnu - # profile: minimal - # override: true - - # - name: Generate Cargo.lock - # uses: actions-rs/cargo@v1 - # with: { command: generate-lockfile } - # - name: Cache Dependencies - # uses: Swatinem/rust-cache@v1.2.0 - - # - name: Generate coverage file - # run: | - # cargo install cargo-tarpaulin --vers "^0.13" - # cargo tarpaulin --workspace --features=rustls,openssl --out Xml --verbose - # - name: Upload to Codecov - # uses: codecov/codecov-action@v1 - # with: { file: cobertura.xml } - nextest: name: nextest runs-on: ubuntu-latest diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f41aa972f..7bb911f79 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,19 +81,37 @@ jobs: cargo test --lib --tests -p=actix-multipart --all-features cargo test --lib --tests -p=actix-web-actors --all-features + - name: Clear the cargo caches + run: | + cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean + cargo-cache + + io-uring: + name: io-uring tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable-x86_64-unknown-linux-gnu + profile: minimal + override: true + + - name: Generate Cargo.lock + uses: actions-rs/cargo@v1 + with: { command: generate-lockfile } + - name: Cache Dependencies + uses: Swatinem/rust-cache@v1.3.0 + - name: tests (io-uring) - if: matrix.target.os == 'ubuntu-latest' timeout-minutes: 60 run: > sudo bash -c "ulimit -Sl 512 && ulimit -Hl 512 && PATH=$PATH:/usr/share/rust/.cargo/bin - && RUSTUP_TOOLCHAIN=${{ matrix.version }} cargo test --lib --tests -p=actix-files --all-features" - - - name: Clear the cargo caches - run: | - cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean - cargo-cache + && RUSTUP_TOOLCHAIN=stable cargo test --lib --tests -p=actix-files --all-features" rustdoc: name: doc tests diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 000000000..137a413d0 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,36 @@ +# disabled because `cargo tarpaulin` currently segfaults + +name: Coverage + +on: + push: + branches: [master] + +jobs: + # job currently (1st Feb 2022) segfaults + coverage: + name: coverage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install stable + uses: actions-rs/toolchain@v1 + with: + toolchain: stable-x86_64-unknown-linux-gnu + profile: minimal + override: true + + - name: Generate Cargo.lock + uses: actions-rs/cargo@v1 + with: { command: generate-lockfile } + - name: Cache Dependencies + uses: Swatinem/rust-cache@v1.2.0 + + - name: Generate coverage file + run: | + cargo install cargo-tarpaulin --vers "^0.13" + cargo tarpaulin --workspace --features=rustls,openssl --out Xml --verbose + - name: Upload to Codecov + uses: codecov/codecov-action@v1 + with: { file: cobertura.xml } diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 4d4c790e8..7e99c2ae1 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,8 +1,11 @@ # Changes ## Unreleased - 2021-xx-xx -- Add support for streaming audio files by setting the `content-disposition` header `inline` instead of `attachement`. [#2645] +- Add `NamedFile::{modified, metadata, content_type, content_disposition, encoding}()` getters. [#2021] +- Update `tokio-uring` dependency to `0.3`. +- Audio files now use `Content-Disposition: inline` instead of `attachment`. [#2645] +[#2021]: https://github.com/actix/actix-web/pull/2021 [#2645]: https://github.com/actix/actix-web/pull/2645 diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index e7e6aea23..8f856c109 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -39,10 +39,12 @@ mime_guess = "2.0.1" percent-encoding = "2.1" pin-project-lite = "0.2.7" +# experimental-io-uring tokio-uring = { version = "0.2", optional = true, features = ["bytes"] } +actix-server = "2.1" # ensure matching tokio-uring versions [dev-dependencies] -actix-rt = "2.2" +actix-rt = "2.7" actix-test = "0.1.0-beta.13" actix-web = "4.0.0" tempfile = "3.2" diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 41113f2ab..40327e5e8 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -364,20 +364,43 @@ mod tests { ); } + #[allow(deprecated)] #[actix_rt::test] - async fn test_named_file_status_code_text() { - let mut file = NamedFile::open_async("Cargo.toml") + async fn status_code_customize_same_output() { + let file1 = NamedFile::open_async("Cargo.toml") .await .unwrap() .set_status_code(StatusCode::NOT_FOUND); + + let file2 = NamedFile::open_async("Cargo.toml") + .await + .unwrap() + .customize() + .with_status(StatusCode::NOT_FOUND); + + let req = TestRequest::default().to_http_request(); + let res1 = file1.respond_to(&req); + let res2 = file2.respond_to(&req); + + assert_eq!(res1.status(), StatusCode::NOT_FOUND); + assert_eq!(res2.status(), StatusCode::NOT_FOUND); + } + + #[actix_rt::test] + async fn test_named_file_status_code_text() { + let mut file = NamedFile::open_async("Cargo.toml").await.unwrap(); + { file.file(); let _f: &File = &file; } + { let _f: &mut File = &mut file; } + let file = file.customize().with_status(StatusCode::NOT_FOUND); + let req = TestRequest::default().to_http_request(); let resp = file.respond_to(&req); assert_eq!( diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 6f3c6e1c8..5580e6f7e 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -23,6 +23,7 @@ use actix_web::{ use bitflags::bitflags; use derive_more::{Deref, DerefMut}; use futures_core::future::LocalBoxFuture; +use mime::Mime; use mime_guess::from_path; use crate::{encoding::equiv_utf8_text, range::HttpRange}; @@ -76,8 +77,8 @@ pub struct NamedFile { pub(crate) md: Metadata, pub(crate) flags: Flags, pub(crate) status_code: StatusCode, - pub(crate) content_type: mime::Mime, - pub(crate) content_disposition: header::ContentDisposition, + pub(crate) content_type: Mime, + pub(crate) content_disposition: ContentDisposition, pub(crate) encoding: Option, } @@ -209,11 +210,10 @@ impl NamedFile { Self::from_file(file, path) } - #[allow(rustdoc::broken_intra_doc_links)] /// Attempts to open a file asynchronously in read-only mode. /// - /// When the `experimental-io-uring` crate feature is enabled, this will be async. - /// Otherwise, it will be just like [`open`][Self::open]. + /// When the `experimental-io-uring` crate feature is enabled, this will be async. Otherwise, it + /// will behave just like `open`. /// /// # Examples /// ``` @@ -238,13 +238,13 @@ impl NamedFile { Self::from_file(file, path) } - /// Returns reference to the underlying `File` object. + /// Returns reference to the underlying file object. #[inline] pub fn file(&self) -> &File { &self.file } - /// Retrieve the path of this file. + /// Returns the filesystem path to this file. /// /// # Examples /// ``` @@ -262,16 +262,53 @@ impl NamedFile { self.path.as_path() } - /// Set response **Status Code** + /// Returns the time the file was last modified. + /// + /// Returns `None` only on unsupported platforms; see [`std::fs::Metadata::modified()`]. + /// Therefore, it is usually safe to unwrap this. + #[inline] + pub fn modified(&self) -> Option { + self.modified + } + + /// Returns the filesystem metadata associated with this file. + #[inline] + pub fn metadata(&self) -> &Metadata { + &self.md + } + + /// Returns the `Content-Type` header that will be used when serving this file. + #[inline] + pub fn content_type(&self) -> &Mime { + &self.content_type + } + + /// Returns the `Content-Disposition` that will be used when serving this file. + #[inline] + pub fn content_disposition(&self) -> &ContentDisposition { + &self.content_disposition + } + + /// Returns the `Content-Encoding` that will be used when serving this file. + /// + /// A return value of `None` indicates that the content is not already using a compressed + /// representation and may be subject to compression downstream. + #[inline] + pub fn content_encoding(&self) -> Option { + self.encoding + } + + /// Set response status code. + #[deprecated(since = "0.7.0", note = "Prefer `Responder::customize()`.")] pub fn set_status_code(mut self, status: StatusCode) -> Self { self.status_code = status; self } - /// Set the MIME Content-Type for serving this file. By default the Content-Type is inferred - /// from the filename extension. + /// Sets the `Content-Type` header that will be used when serving this file. By default the + /// `Content-Type` is inferred from the filename extension. #[inline] - pub fn set_content_type(mut self, mime_type: mime::Mime) -> Self { + pub fn set_content_type(mut self, mime_type: Mime) -> Self { self.content_type = mime_type; self } @@ -284,15 +321,15 @@ impl NamedFile { /// filename is taken from the path provided in the `open` method after converting it to UTF-8 /// (using `to_string_lossy`). #[inline] - pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self { + pub fn set_content_disposition(mut self, cd: ContentDisposition) -> Self { self.content_disposition = cd; self.flags.insert(Flags::CONTENT_DISPOSITION); self } - /// Disable `Content-Disposition` header. + /// Disables `Content-Disposition` header. /// - /// By default Content-Disposition` header is enabled. + /// By default, the `Content-Disposition` header is sent. #[inline] pub fn disable_content_disposition(mut self) -> Self { self.flags.remove(Flags::CONTENT_DISPOSITION); diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index e2a2bcc3d..6f7563ffa 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -29,13 +29,13 @@ default = [] openssl = ["tls-openssl", "awc/openssl"] [dependencies] -actix-service = "2.0.0" +actix-service = "2" actix-codec = "0.5" actix-tls = "3" -actix-utils = "3.0.0" +actix-utils = "3" actix-rt = "2.2" actix-server = "2" -awc = { version = "3.0.0-beta.21", default-features = false } +awc = { version = "3", default-features = false } base64 = "0.13" bytes = "1" @@ -51,5 +51,5 @@ tls-openssl = { version = "0.10.9", package = "openssl", optional = true } tokio = { version = "1.8.4", features = ["sync"] } [dev-dependencies] -actix-web = { version = "4.0.0", default-features = false, features = ["cookies"] } -actix-http = "3.0.0" +actix-web = { version = "4", default-features = false, features = ["cookies"] } +actix-http = "3" diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index d73e8522f..71132c6b2 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -3,6 +3,30 @@ ## Unreleased - 2021-xx-xx +## 3.0.4 - 2022-03-09 +### Fixed +- Document on docs.rs with `ws` feature enabled. + + +## 3.0.3 - 2022-03-08 +### Fixed +- Allow spaces between header name and colon when parsing responses. [#2684] + +[#2684]: https://github.com/actix/actix-web/issues/2684 + + +## 3.0.2 - 2022-03-05 +### Fixed +- Fix encoding camel-case header names with more than one hyphen. [#2683] + +[#2683]: https://github.com/actix/actix-web/issues/2683 + + +## 3.0.1 - 2022-03-04 +- Fix panic in H1 dispatcher when pipelining is used with keep-alive. [#2678] + +[#2678]: https://github.com/actix/actix-web/issues/2678 + ## 3.0.0 - 2022-02-25 ### Dependencies - Updated `actix-*` to Tokio v1-based versions. [#1813] @@ -745,10 +769,10 @@ - Remove `ResponseError` impl for `actix::actors::resolver::ResolverError` due to deprecate of resolver actor. [#1813] - Remove `ConnectError::SslHandshakeError` and re-export of `HandshakeError`. - due to the removal of this type from `tokio-openssl` crate. openssl handshake + due to the removal of this type from `tokio-openssl` crate. openssl handshake error would return as `ConnectError::SslError`. [#1813] - Remove `actix-threadpool` dependency. Use `actix_rt::task::spawn_blocking`. - Due to this change `actix_threadpool::BlockingError` type is moved into + Due to this change `actix_threadpool::BlockingError` type is moved into `actix_http::error` module. [#1878] [#1813]: https://github.com/actix/actix-web/pull/1813 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 751b820e8..6d410e46f 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http" -version = "3.0.0" +version = "3.0.4" authors = [ "Nikolay Kim ", "Rob Ede ", @@ -20,7 +20,7 @@ edition = "2018" [package.metadata.docs.rs] # features that docs.rs will build with -features = ["http2", "openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd"] +features = ["http2", "ws", "openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd"] [lib] name = "actix_http" @@ -73,11 +73,11 @@ httparse = "1.5.1" httpdate = "1.0.1" itoa = "1" language-tags = "0.3" -log = "0.4" mime = "0.3" percent-encoding = "2.1" pin-project-lite = "0.2" smallvec = "1.6.1" +tracing = { version = "0.1.30", default-features = false, features = ["log"] } # http2 h2 = { version = "0.3.9", optional = true } @@ -110,6 +110,7 @@ memchr = "2.4" once_cell = "1.9" rcgen = "0.8" regex = "1.3" +rustversion = "1" rustls-pemfile = "0.2" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" @@ -120,7 +121,7 @@ tokio = { version = "1.8.4", features = ["net", "rt", "macros"] } [[example]] name = "ws" -required-features = ["rustls"] +required-features = ["ws", "rustls"] [[bench]] name = "write-camel-case" diff --git a/actix-http/README.md b/actix-http/README.md index 8d414a0fc..14a7013db 100644 --- a/actix-http/README.md +++ b/actix-http/README.md @@ -3,11 +3,11 @@ > HTTP primitives for the Actix ecosystem. [![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) -[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0)](https://docs.rs/actix-http/3.0.0) +[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.4)](https://docs.rs/actix-http/3.0.4) [![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
-[![dependency status](https://deps.rs/crate/actix-http/3.0.0/status.svg)](https://deps.rs/crate/actix-http/3.0.0) +[![dependency status](https://deps.rs/crate/actix-http/3.0.4/status.svg)](https://deps.rs/crate/actix-http/3.0.4) [![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) @@ -25,7 +25,7 @@ use actix_http::{HttpService, Response}; use actix_server::Server; use futures_util::future; use http::header::HeaderValue; -use log::info; +use tracing::info; #[actix_rt::main] async fn main() -> io::Result<()> { diff --git a/actix-http/examples/actix-web.rs b/actix-http/examples/actix-web.rs index f8226507f..449e5899b 100644 --- a/actix-http/examples/actix-web.rs +++ b/actix-http/examples/actix-web.rs @@ -18,7 +18,8 @@ async fn main() -> std::io::Result<()> { HttpService::build() // pass the app to service builder // map_config is used to map App's configuration to ServiceBuilder - .finish(map_config(app, |_| AppConfig::default())) + // h1 will configure server to only use HTTP/1.1 + .h1(map_config(app, |_| AppConfig::default())) .tcp() })? .run() diff --git a/actix-http/examples/echo.rs b/actix-http/examples/echo.rs index 58de64530..ae6f00cce 100644 --- a/actix-http/examples/echo.rs +++ b/actix-http/examples/echo.rs @@ -5,6 +5,7 @@ use actix_server::Server; use bytes::BytesMut; use futures_util::StreamExt as _; use http::header::HeaderValue; +use tracing::info; #[actix_rt::main] async fn main() -> io::Result<()> { @@ -22,7 +23,7 @@ async fn main() -> io::Result<()> { body.extend_from_slice(&item?); } - log::info!("request body: {:?}", body); + info!("request body: {:?}", body); let res = Response::build(StatusCode::OK) .insert_header(("x-head", HeaderValue::from_static("dummy value!"))) diff --git a/actix-http/examples/hello-world.rs b/actix-http/examples/hello-world.rs index 1a83d4d9c..c749cdd00 100644 --- a/actix-http/examples/hello-world.rs +++ b/actix-http/examples/hello-world.rs @@ -1,9 +1,8 @@ use std::{convert::Infallible, io, time::Duration}; -use actix_http::{ - header::HeaderValue, HttpMessage, HttpService, Request, Response, StatusCode, -}; +use actix_http::{header::HeaderValue, HttpService, Request, Response, StatusCode}; use actix_server::Server; +use tracing::info; #[actix_rt::main] async fn main() -> io::Result<()> { @@ -18,12 +17,12 @@ async fn main() -> io::Result<()> { ext.insert(42u32); }) .finish(|req: Request| async move { - log::info!("{:?}", req); + info!("{:?}", req); let mut res = Response::build(StatusCode::OK); res.insert_header(("x-head", HeaderValue::from_static("dummy value!"))); - let forty_two = req.extensions().get::().unwrap().to_string(); + let forty_two = req.conn_data::().unwrap().to_string(); res.insert_header(( "x-forty-two", HeaderValue::from_str(&forty_two).unwrap(), diff --git a/actix-http/examples/streaming-error.rs b/actix-http/examples/streaming-error.rs index 3988cbac2..8c8a249cb 100644 --- a/actix-http/examples/streaming-error.rs +++ b/actix-http/examples/streaming-error.rs @@ -12,6 +12,7 @@ use actix_http::{body::BodyStream, HttpService, Response}; use actix_server::Server; use async_stream::stream; use bytes::Bytes; +use tracing::info; #[actix_rt::main] async fn main() -> io::Result<()> { @@ -21,7 +22,7 @@ async fn main() -> io::Result<()> { .bind("streaming-error", ("127.0.0.1", 8080), || { HttpService::build() .finish(|req| async move { - log::info!("{:?}", req); + info!("{:?}", req); let res = Response::ok(); Ok::<_, Infallible>(res.set_body(BodyStream::new(stream! { diff --git a/actix-http/examples/ws.rs b/actix-http/examples/ws.rs index d70e43314..c4f0503cd 100644 --- a/actix-http/examples/ws.rs +++ b/actix-http/examples/ws.rs @@ -17,6 +17,7 @@ use actix_server::Server; use bytes::{Bytes, BytesMut}; use bytestring::ByteString; use futures_core::{ready, Stream}; +use tracing::{info, trace}; #[actix_rt::main] async fn main() -> io::Result<()> { @@ -34,13 +35,13 @@ async fn main() -> io::Result<()> { } async fn handler(req: Request) -> Result>, Error> { - log::info!("handshaking"); + info!("handshaking"); let mut res = ws::handshake(req.head())?; // handshake will always fail under HTTP/2 - log::info!("responding"); - Ok(res.message_body(BodyStream::new(Heartbeat::new(ws::Codec::new())))?) + info!("responding"); + res.message_body(BodyStream::new(Heartbeat::new(ws::Codec::new()))) } struct Heartbeat { @@ -61,7 +62,7 @@ impl Stream for Heartbeat { type Item = Result; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - log::trace!("poll"); + trace!("poll"); ready!(self.as_mut().interval.poll_tick(cx)); diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs index 0c81ffe1b..0bbb1c106 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -17,6 +17,7 @@ use pin_project_lite::pin_project; #[cfg(feature = "compress-gzip")] use flate2::write::{GzEncoder, ZlibEncoder}; +use tracing::trace; #[cfg(feature = "compress-zstd")] use zstd::stream::write::Encoder as ZstdEncoder; @@ -356,7 +357,7 @@ impl ContentEncoder { ContentEncoder::Brotli(ref mut encoder) => match encoder.write_all(data) { Ok(_) => Ok(()), Err(err) => { - log::trace!("Error decoding br encoding: {}", err); + trace!("Error decoding br encoding: {}", err); Err(err) } }, @@ -365,7 +366,7 @@ impl ContentEncoder { ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) { Ok(_) => Ok(()), Err(err) => { - log::trace!("Error decoding gzip encoding: {}", err); + trace!("Error decoding gzip encoding: {}", err); Err(err) } }, @@ -374,7 +375,7 @@ impl ContentEncoder { ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) { Ok(_) => Ok(()), Err(err) => { - log::trace!("Error decoding deflate encoding: {}", err); + trace!("Error decoding deflate encoding: {}", err); Err(err) } }, @@ -383,7 +384,7 @@ impl ContentEncoder { ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) { Ok(_) => Ok(()), Err(err) => { - log::trace!("Error decoding ztsd encoding: {}", err); + trace!("Error decoding ztsd encoding: {}", err); Err(err) } }, diff --git a/actix-http/src/h1/chunked.rs b/actix-http/src/h1/chunked.rs index 7d0532fcd..c1dd4b283 100644 --- a/actix-http/src/h1/chunked.rs +++ b/actix-http/src/h1/chunked.rs @@ -1,6 +1,7 @@ use std::{io, task::Poll}; use bytes::{Buf as _, Bytes, BytesMut}; +use tracing::{debug, trace}; macro_rules! byte ( ($rdr:ident) => ({ @@ -76,7 +77,7 @@ impl ChunkedState { Poll::Ready(Ok(ChunkedState::Size)) } None => { - log::debug!("chunk size would overflow u64"); + debug!("chunk size would overflow u64"); Poll::Ready(Err(io::Error::new( io::ErrorKind::InvalidInput, "Invalid chunk size line: Size is too big", @@ -124,7 +125,7 @@ impl ChunkedState { rem: &mut u64, buf: &mut Option, ) -> Poll> { - log::trace!("Chunked read, remaining={:?}", rem); + trace!("Chunked read, remaining={:?}", rem); let len = rdr.len() as u64; if len == 0 { diff --git a/actix-http/src/h1/decoder.rs b/actix-http/src/h1/decoder.rs index 17b9b695c..a9443997e 100644 --- a/actix-http/src/h1/decoder.rs +++ b/actix-http/src/h1/decoder.rs @@ -6,7 +6,7 @@ use http::{ header::{self, HeaderName, HeaderValue}, Method, StatusCode, Uri, Version, }; -use log::{debug, error, trace}; +use tracing::{debug, error, trace}; use super::chunked::ChunkedState; use crate::{error::ParseError, header::HeaderMap, ConnectionType, Request, ResponseHead}; @@ -293,22 +293,35 @@ impl MessageType for ResponseHead { let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY; let (len, ver, status, h_len) = { - let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY; + // SAFETY: + // Create an uninitialized array of `MaybeUninit`. The `assume_init` is safe because the + // type we are claiming to have initialized here is a bunch of `MaybeUninit`s, which + // do not require initialization. + let mut parsed = unsafe { + MaybeUninit::<[MaybeUninit>; MAX_HEADERS]>::uninit() + .assume_init() + }; - let mut res = httparse::Response::new(&mut parsed); - match res.parse(src)? { + let mut res = httparse::Response::new(&mut []); + + let mut config = httparse::ParserConfig::default(); + config.allow_spaces_after_header_name_in_responses(true); + + match config.parse_response_with_uninit_headers(&mut res, src, &mut parsed)? { httparse::Status::Complete(len) => { let version = if res.version.unwrap() == 1 { Version::HTTP_11 } else { Version::HTTP_10 }; + let status = StatusCode::from_u16(res.code.unwrap()) .map_err(|_| ParseError::Status)?; HeaderIndex::record(src, res.headers, &mut headers); (len, version, status, res.headers.len()) } + httparse::Status::Partial => { return if src.len() >= MAX_BUFFER_SIZE { error!("MAX_BUFFER_SIZE unprocessed data reached, closing"); @@ -360,9 +373,6 @@ pub(crate) const EMPTY_HEADER_INDEX: HeaderIndex = HeaderIndex { pub(crate) const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] = [EMPTY_HEADER_INDEX; MAX_HEADERS]; -pub(crate) const EMPTY_HEADER_ARRAY: [httparse::Header<'static>; MAX_HEADERS] = - [httparse::EMPTY_HEADER; MAX_HEADERS]; - impl HeaderIndex { pub(crate) fn record( bytes: &[u8], diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index f029fb108..dea8a4beb 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -15,6 +15,7 @@ use bitflags::bitflags; use bytes::{Buf, BytesMut}; use futures_core::ready; use pin_project_lite::pin_project; +use tracing::{debug, error, trace}; use crate::{ body::{BodySize, BoxBody, MessageBody}, @@ -336,7 +337,7 @@ where while written < len { match io.as_mut().poll_write(cx, &write_buf[written..])? { Poll::Ready(0) => { - log::error!("write zero; closing"); + error!("write zero; closing"); return Poll::Ready(Err(io::Error::new(io::ErrorKind::WriteZero, ""))); } @@ -375,8 +376,6 @@ where DispatchError::Io(err) })?; - this.flags.set(Flags::KEEP_ALIVE, this.codec.keep_alive()); - Ok(size) } @@ -459,7 +458,12 @@ where } // all messages are dealt with - None => return Ok(PollResponse::DoNothing), + None => { + // start keep-alive if last request allowed it + this.flags.set(Flags::KEEP_ALIVE, this.codec.keep_alive()); + + return Ok(PollResponse::DoNothing); + } }, StateProj::ServiceCall { fut } => { @@ -565,7 +569,7 @@ where } StateProj::ExpectCall { fut } => { - log::trace!(" calling expect service"); + trace!(" calling expect service"); match fut.poll(cx) { // expect resolved. write continue to buffer and set InnerDispatcher state @@ -694,12 +698,12 @@ where let mut this = self.as_mut().project(); if can_not_read { - log::debug!("cannot read request payload"); + debug!("cannot read request payload"); if let Some(sender) = &this.payload { // ...maybe handler does not want to read any more payload... if let PayloadStatus::Dropped = sender.need_read(cx) { - log::debug!("handler dropped payload early; attempt to clean connection"); + debug!("handler dropped payload early; attempt to clean connection"); // ...in which case poll request payload a few times loop { match this.codec.decode(this.read_buf)? { @@ -713,7 +717,7 @@ where // connection is in clean state for next request Message::Chunk(None) => { - log::debug!("connection successfully cleaned"); + debug!("connection successfully cleaned"); // reset dispatcher state let _ = this.payload.take(); @@ -734,7 +738,7 @@ where // not enough info to decide if connection is going to be clean or not None => { - log::error!( + error!( "handler did not read whole payload and dispatcher could not \ drain read buf; return 500 and close connection" ); @@ -757,6 +761,7 @@ where let mut updated = false; + // decode from read buf as many full requests as possible loop { match this.codec.decode(this.read_buf) { Ok(Some(msg)) => { @@ -809,7 +814,7 @@ where if let Some(ref mut payload) = this.payload { payload.feed_data(chunk); } else { - log::error!("Internal server error: unexpected payload chunk"); + error!("Internal server error: unexpected payload chunk"); this.flags.insert(Flags::READ_DISCONNECT); this.messages.push_back(DispatcherMessage::Error( Response::internal_server_error().drop_body(), @@ -823,7 +828,7 @@ where if let Some(mut payload) = this.payload.take() { payload.feed_eof(); } else { - log::error!("Internal server error: unexpected eof"); + error!("Internal server error: unexpected eof"); this.flags.insert(Flags::READ_DISCONNECT); this.messages.push_back(DispatcherMessage::Error( Response::internal_server_error().drop_body(), @@ -840,7 +845,7 @@ where Ok(None) => break, Err(ParseError::Io(err)) => { - log::trace!("I/O error: {}", &err); + trace!("I/O error: {}", &err); self.as_mut().client_disconnected(); this = self.as_mut().project(); *this.error = Some(DispatchError::Io(err)); @@ -848,7 +853,7 @@ where } Err(ParseError::TooLarge) => { - log::trace!("request head was too big; returning 431 response"); + trace!("request head was too big; returning 431 response"); if let Some(mut payload) = this.payload.take() { payload.set_error(PayloadError::Overflow); @@ -868,7 +873,7 @@ where } Err(err) => { - log::trace!("parse error {}", &err); + trace!("parse error {}", &err); if let Some(mut payload) = this.payload.take() { payload.set_error(PayloadError::EncodingCorrupted); @@ -899,7 +904,7 @@ where if timer.as_mut().poll(cx).is_ready() { // timeout on first request (slow request) return 408 - log::trace!( + trace!( "timed out on slow request; \ replying with 408 and closing connection" ); @@ -945,7 +950,7 @@ where // keep-alive timer has timed out if timer.as_mut().poll(cx).is_ready() { // no tasks at hand - log::trace!("timer timed out; closing connection"); + trace!("timer timed out; closing connection"); this.flags.insert(Flags::SHUTDOWN); if let Some(deadline) = this.config.client_disconnect_deadline() { @@ -975,7 +980,7 @@ where // timed-out during shutdown; drop connection if timer.as_mut().poll(cx).is_ready() { - log::trace!("timed-out during shutdown"); + trace!("timed-out during shutdown"); return Err(DispatchError::DisconnectTimeout); } } @@ -1134,12 +1139,12 @@ where match this.inner.project() { DispatcherStateProj::Upgrade { fut: upgrade } => upgrade.poll(cx).map_err(|err| { - log::error!("Upgrade handler error: {}", err); + error!("Upgrade handler error: {}", err); DispatchError::Upgrade }), DispatcherStateProj::Normal { mut inner } => { - log::trace!("start flags: {:?}", &inner.flags); + trace!("start flags: {:?}", &inner.flags); trace_timer_states( "start", @@ -1246,7 +1251,7 @@ where // client is gone if inner.flags.contains(Flags::WRITE_DISCONNECT) { - log::trace!("client is gone; disconnecting"); + trace!("client is gone; disconnecting"); return Poll::Ready(Ok(())); } @@ -1255,14 +1260,14 @@ where // read half is closed; we do not process any responses if inner_p.flags.contains(Flags::READ_DISCONNECT) && state_is_none { - log::trace!("read half closed; start shutdown"); + trace!("read half closed; start shutdown"); inner_p.flags.insert(Flags::SHUTDOWN); } // keep-alive and stream errors if state_is_none && inner_p.write_buf.is_empty() { if let Some(err) = inner_p.error.take() { - log::error!("stream error: {}", &err); + error!("stream error: {}", &err); return Poll::Ready(Err(err)); } @@ -1291,7 +1296,7 @@ where Poll::Pending }; - log::trace!("end flags: {:?}", &inner.flags); + trace!("end flags: {:?}", &inner.flags); poll } @@ -1306,17 +1311,17 @@ fn trace_timer_states( ka_timer: &TimerState, shutdown_timer: &TimerState, ) { - log::trace!("{} timers:", label); + trace!("{} timers:", label); if head_timer.is_enabled() { - log::trace!(" head {}", &head_timer); + trace!(" head {}", &head_timer); } if ka_timer.is_enabled() { - log::trace!(" keep-alive {}", &ka_timer); + trace!(" keep-alive {}", &ka_timer); } if shutdown_timer.is_enabled() { - log::trace!(" shutdown {}", &shutdown_timer); + trace!(" shutdown {}", &shutdown_timer); } } diff --git a/actix-http/src/h1/encoder.rs b/actix-http/src/h1/encoder.rs index ba98f4641..21cfd75c4 100644 --- a/actix-http/src/h1/encoder.rs +++ b/actix-http/src/h1/encoder.rs @@ -517,6 +517,7 @@ unsafe fn write_camel_case(value: &[u8], buf: *mut u8, len: usize) { if let Some(c @ b'a'..=b'z') = iter.next() { buffer[index] = c & 0b1101_1111; } + index += 1; } index += 1; @@ -528,7 +529,7 @@ mod tests { use std::rc::Rc; use bytes::Bytes; - use http::header::AUTHORIZATION; + use http::header::{AUTHORIZATION, UPGRADE_INSECURE_REQUESTS}; use super::*; use crate::{ @@ -559,6 +560,9 @@ mod tests { head.headers .insert(CONTENT_TYPE, HeaderValue::from_static("plain/text")); + head.headers + .insert(UPGRADE_INSECURE_REQUESTS, HeaderValue::from_static("1")); + let mut head = RequestHeadType::Owned(head); let _ = head.encode_headers( @@ -574,6 +578,7 @@ mod tests { assert!(data.contains("Connection: close\r\n")); assert!(data.contains("Content-Type: plain/text\r\n")); assert!(data.contains("Date: date\r\n")); + assert!(data.contains("Upgrade-Insecure-Requests: 1\r\n")); let _ = head.encode_headers( &mut bytes, diff --git a/actix-http/src/h1/payload.rs b/actix-http/src/h1/payload.rs index 4d031c15a..5a93e9051 100644 --- a/actix-http/src/h1/payload.rs +++ b/actix-http/src/h1/payload.rs @@ -263,7 +263,10 @@ mod tests { assert_not_impl_any!(Payload: Send, Sync, UnwindSafe, RefUnwindSafe); assert_impl_all!(Inner: Unpin, Send, Sync); + #[rustversion::before(1.60)] assert_not_impl_any!(Inner: UnwindSafe, RefUnwindSafe); + #[rustversion::since(1.60)] + assert_impl_all!(Inner: UnwindSafe, RefUnwindSafe); #[actix_rt::test] async fn test_unread_data() { diff --git a/actix-http/src/h1/service.rs b/actix-http/src/h1/service.rs index 43b7919a7..a791ea8c3 100644 --- a/actix-http/src/h1/service.rs +++ b/actix-http/src/h1/service.rs @@ -13,6 +13,7 @@ use actix_service::{ }; use actix_utils::future::ready; use futures_core::future::LocalBoxFuture; +use tracing::error; use crate::{ body::{BoxBody, MessageBody}, @@ -305,13 +306,13 @@ where Box::pin(async move { let expect = expect .await - .map_err(|e| log::error!("Init http expect service error: {:?}", e))?; + .map_err(|e| error!("Init http expect service error: {:?}", e))?; let upgrade = match upgrade { Some(upgrade) => { let upgrade = upgrade .await - .map_err(|e| log::error!("Init http upgrade service error: {:?}", e))?; + .map_err(|e| error!("Init http upgrade service error: {:?}", e))?; Some(upgrade) } None => None, @@ -319,7 +320,7 @@ where let service = service .await - .map_err(|e| log::error!("Init http service error: {:?}", e))?; + .map_err(|e| error!("Init http service error: {:?}", e))?; Ok(H1ServiceHandler::new( cfg, @@ -357,7 +358,7 @@ where fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { self._poll_ready(cx).map_err(|err| { - log::error!("HTTP/1 service readiness error: {:?}", err); + error!("HTTP/1 service readiness error: {:?}", err); DispatchError::Service(err) }) } diff --git a/actix-http/src/h1/timer.rs b/actix-http/src/h1/timer.rs index bb69fdb80..0ea70a319 100644 --- a/actix-http/src/h1/timer.rs +++ b/actix-http/src/h1/timer.rs @@ -1,6 +1,7 @@ use std::{fmt, future::Future, pin::Pin, task::Context}; use actix_rt::time::{Instant, Sleep}; +use tracing::trace; #[derive(Debug)] pub(super) enum TimerState { @@ -24,7 +25,7 @@ impl TimerState { pub(super) fn set(&mut self, timer: Sleep, line: u32) { if matches!(self, Self::Disabled) { - log::trace!("setting disabled timer from line {}", line); + trace!("setting disabled timer from line {}", line); } *self = Self::Active { @@ -39,11 +40,11 @@ impl TimerState { pub(super) fn clear(&mut self, line: u32) { if matches!(self, Self::Disabled) { - log::trace!("trying to clear a disabled timer from line {}", line); + trace!("trying to clear a disabled timer from line {}", line); } if matches!(self, Self::Inactive) { - log::trace!("trying to clear an inactive timer from line {}", line); + trace!("trying to clear an inactive timer from line {}", line); } *self = Self::Inactive; diff --git a/actix-http/src/h2/dispatcher.rs b/actix-http/src/h2/dispatcher.rs index ce1be537f..85516cccc 100644 --- a/actix-http/src/h2/dispatcher.rs +++ b/actix-http/src/h2/dispatcher.rs @@ -19,8 +19,8 @@ use h2::{ server::{Connection, SendResponse}, Ping, PingPong, }; -use log::{error, trace}; use pin_project_lite::pin_project; +use tracing::{error, trace, warn}; use crate::{ body::{BodySize, BoxBody, MessageBody}, @@ -143,7 +143,7 @@ where DispatchError::SendResponse(err) => { trace!("Error sending HTTP/2 response: {:?}", err) } - DispatchError::SendData(err) => log::warn!("{:?}", err), + DispatchError::SendData(err) => warn!("{:?}", err), DispatchError::ResponseBody(err) => { error!("Response payload stream error: {:?}", err) } diff --git a/actix-http/src/h2/service.rs b/actix-http/src/h2/service.rs index 653982d37..e526918c7 100644 --- a/actix-http/src/h2/service.rs +++ b/actix-http/src/h2/service.rs @@ -14,7 +14,7 @@ use actix_service::{ }; use actix_utils::future::ready; use futures_core::{future::LocalBoxFuture, ready}; -use log::error; +use tracing::{error, trace}; use crate::{ body::{BoxBody, MessageBody}, @@ -355,7 +355,7 @@ where } Err(err) => { - log::trace!("H2 handshake error: {}", err); + trace!("H2 handshake error: {}", err); Poll::Ready(Err(err)) } }, diff --git a/actix-http/src/payload.rs b/actix-http/src/payload.rs index 33d9ec6f5..ee0128af4 100644 --- a/actix-http/src/payload.rs +++ b/actix-http/src/payload.rs @@ -13,7 +13,8 @@ use crate::error::PayloadError; /// A boxed payload stream. pub type BoxedPayloadStream = Pin>>>; -#[deprecated(since = "4.0.0", note = "Renamed to `BoxedPayloadStream`.")] +#[doc(hidden)] +#[deprecated(since = "3.0.0", note = "Renamed to `BoxedPayloadStream`.")] pub type PayloadStream = BoxedPayloadStream; #[cfg(not(feature = "http2"))] diff --git a/actix-http/src/responses/builder.rs b/actix-http/src/responses/builder.rs index 4a67423b1..063af92da 100644 --- a/actix-http/src/responses/builder.rs +++ b/actix-http/src/responses/builder.rs @@ -144,7 +144,7 @@ impl ResponseBuilder { self } - /// Set connection type to Upgrade + /// Set connection type to `Upgrade`. #[inline] pub fn upgrade(&mut self, value: V) -> &mut Self where @@ -161,7 +161,7 @@ impl ResponseBuilder { self } - /// Force close connection, even if it is marked as keep-alive + /// Force-close connection, even if it is marked as keep-alive. #[inline] pub fn force_close(&mut self) -> &mut Self { if let Some(parts) = self.inner() { diff --git a/actix-http/src/service.rs b/actix-http/src/service.rs index b220e55a4..f4fe625a3 100644 --- a/actix-http/src/service.rs +++ b/actix-http/src/service.rs @@ -15,6 +15,7 @@ use actix_service::{ }; use futures_core::{future::LocalBoxFuture, ready}; use pin_project_lite::pin_project; +use tracing::error; use crate::{ body::{BoxBody, MessageBody}, @@ -369,13 +370,13 @@ where Box::pin(async move { let expect = expect .await - .map_err(|e| log::error!("Init http expect service error: {:?}", e))?; + .map_err(|e| error!("Init http expect service error: {:?}", e))?; let upgrade = match upgrade { Some(upgrade) => { let upgrade = upgrade .await - .map_err(|e| log::error!("Init http upgrade service error: {:?}", e))?; + .map_err(|e| error!("Init http upgrade service error: {:?}", e))?; Some(upgrade) } None => None, @@ -383,7 +384,7 @@ where let service = service .await - .map_err(|e| log::error!("Init http service error: {:?}", e))?; + .map_err(|e| error!("Init http service error: {:?}", e))?; Ok(HttpServiceHandler::new( cfg, @@ -490,7 +491,7 @@ where fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { self._poll_ready(cx).map_err(|err| { - log::error!("HTTP service readiness error: {:?}", err); + error!("HTTP service readiness error: {:?}", err); DispatchError::Service(err) }) } @@ -666,7 +667,7 @@ where self.poll(cx) } Err(err) => { - log::trace!("H2 handshake error: {}", err); + tracing::trace!("H2 handshake error: {}", err); Poll::Ready(Err(err)) } } diff --git a/actix-http/src/ws/codec.rs b/actix-http/src/ws/codec.rs index 6e7aa7c11..3aa325d6a 100644 --- a/actix-http/src/ws/codec.rs +++ b/actix-http/src/ws/codec.rs @@ -2,6 +2,7 @@ use actix_codec::{Decoder, Encoder}; use bitflags::bitflags; use bytes::{Bytes, BytesMut}; use bytestring::ByteString; +use tracing::error; use super::{ frame::Parser, @@ -253,7 +254,7 @@ impl Decoder for Codec { } } _ => { - log::error!("Unfinished fragment {:?}", opcode); + error!("Unfinished fragment {:?}", opcode); Err(ProtocolError::ContinuationFragment(opcode)) } }; diff --git a/actix-http/src/ws/dispatcher.rs b/actix-http/src/ws/dispatcher.rs index 4c7470d37..2f6b2363b 100644 --- a/actix-http/src/ws/dispatcher.rs +++ b/actix-http/src/ws/dispatcher.rs @@ -73,8 +73,8 @@ mod inner { use actix_service::{IntoService, Service}; use futures_core::stream::Stream; use local_channel::mpsc; - use log::debug; use pin_project_lite::pin_project; + use tracing::debug; use actix_codec::{AsyncRead, AsyncWrite, Decoder, Encoder, Framed}; diff --git a/actix-http/src/ws/frame.rs b/actix-http/src/ws/frame.rs index 78cef1046..17e34e2ba 100644 --- a/actix-http/src/ws/frame.rs +++ b/actix-http/src/ws/frame.rs @@ -1,7 +1,7 @@ use std::convert::TryFrom; use bytes::{Buf, BufMut, BytesMut}; -use log::debug; +use tracing::debug; use super::{ mask::apply_mask, diff --git a/actix-http/src/ws/proto.rs b/actix-http/src/ws/proto.rs index 4227f221d..01fe9dd3c 100644 --- a/actix-http/src/ws/proto.rs +++ b/actix-http/src/ws/proto.rs @@ -3,6 +3,8 @@ use std::{ fmt, }; +use tracing::error; + /// Operation codes defined in [RFC 6455 ยง11.8]. /// /// [RFC 6455]: https://datatracker.ietf.org/doc/html/rfc6455#section-11.8 @@ -58,7 +60,7 @@ impl From for u8 { Ping => 9, Pong => 10, Bad => { - log::error!("Attempted to convert invalid opcode to u8. This is a bug."); + error!("Attempted to convert invalid opcode to u8. This is a bug."); 8 // if this somehow happens, a close frame will help us tear down quickly } } diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs index 8e59ec65d..550375296 100644 --- a/actix-http/tests/test_rustls.rs +++ b/actix-http/tests/test_rustls.rs @@ -212,6 +212,7 @@ async fn h2_content_length() { let value = HeaderValue::from_static("0"); { + #[allow(clippy::single_element_loop)] for &i in &[0] { let req = srv .request(Method::HEAD, srv.surl(&format!("/{}", i))) @@ -226,6 +227,7 @@ async fn h2_content_length() { // assert_eq!(response.headers().get(&header), None); } + #[allow(clippy::single_element_loop)] for &i in &[1] { let req = srv .request(Method::GET, srv.surl(&format!("/{}", i))) diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 450a57fa9..e93e22941 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -14,7 +14,7 @@ name = "actix_multipart" path = "src/lib.rs" [dependencies] -actix-utils = "3.0.0" +actix-utils = "3" actix-web = { version = "4.0.0", default-features = false } bytes = "1" diff --git a/actix-router/Cargo.toml b/actix-router/Cargo.toml index 502109114..6fcef125d 100644 --- a/actix-router/Cargo.toml +++ b/actix-router/Cargo.toml @@ -23,9 +23,9 @@ default = ["http"] bytestring = ">=0.1.5, <2" firestorm = "0.5" http = { version = "0.2.3", optional = true } -log = "0.4" regex = "1.5" serde = "1" +tracing = { version = "0.1.30", default-features = false, features = ["log"] } [dev-dependencies] criterion = { version = "0.3", features = ["html_reports"] } diff --git a/actix-router/src/resource.rs b/actix-router/src/resource.rs index c616b467a..3d121f369 100644 --- a/actix-router/src/resource.rs +++ b/actix-router/src/resource.rs @@ -7,6 +7,7 @@ use std::{ use firestorm::{profile_fn, profile_method, profile_section}; use regex::{escape, Regex, RegexSet}; +use tracing::error; use crate::{path::PathItem, IntoPatterns, Patterns, Resource, ResourcePath}; @@ -714,10 +715,7 @@ impl ResourceDef { if let Some(m) = captures.name(name) { segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16); } else { - log::error!( - "Dynamic path match but not all segments found: {}", - name - ); + error!("Dynamic path match but not all segments found: {}", name); return false; } } @@ -744,7 +742,7 @@ impl ResourceDef { if let Some(m) = captures.name(name) { segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16); } else { - log::error!("Dynamic path match but not all segments found: {}", name); + error!("Dynamic path match but not all segments found: {}", name); return false; } } @@ -1038,7 +1036,7 @@ impl ResourceDef { // tail segments in prefixes have no defined semantics #[cfg(not(test))] - log::warn!( + tracing::warn!( "Prefix resources should not have tail segments. \ Use `ResourceDef::new` constructor. \ This may become a panic in the future." @@ -1053,7 +1051,7 @@ impl ResourceDef { // unnamed tail segment #[cfg(not(test))] - log::warn!( + tracing::warn!( "Tail segments must have names. \ Consider `.../{{tail}}*`. \ This may become a panic in the future." diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index af4aff56a..9938be67d 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -29,13 +29,13 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"] [dependencies] actix-codec = "0.5" -actix-http = "3.0.0" +actix-http = "3" actix-http-test = "3.0.0-beta.13" actix-rt = "2.1" -actix-service = "2.0.0" -actix-utils = "3.0.0" -actix-web = { version = "4.0.0", default-features = false, features = ["cookies"] } -awc = { version = "3.0.0-beta.21", default-features = false, features = ["cookies"] } +actix-service = "2" +actix-utils = "3" +actix-web = { version = "4", default-features = false, features = ["cookies"] } +awc = { version = "3", default-features = false, features = ["cookies"] } futures-core = { version = "0.3.7", default-features = false, features = ["std"] } futures-util = { version = "0.3.7", default-features = false, features = [] } diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 225326565..c939f6ab5 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -28,7 +28,7 @@ tokio = { version = "1.13.1", features = ["sync"] } [dev-dependencies] actix-rt = "2.2" actix-test = "0.1.0-beta.13" -awc = { version = "3.0.0-beta.21", default-features = false } +awc = { version = "3", default-features = false } env_logger = "0.9" futures-util = { version = "0.3.7", default-features = false } diff --git a/actix-web/CHANGES.md b/actix-web/CHANGES.md index 8ee5c9837..bd2252059 100644 --- a/actix-web/CHANGES.md +++ b/actix-web/CHANGES.md @@ -20,7 +20,7 @@ - Updated `cookie` to `0.16`. [#2555] - Updated `language-tags` to `0.3`. - Updated `rand` to `0.8`. -- Updated `rustls` to `0.20.0`. [#2414] +- Updated `rustls` to `0.20`. [#2414] - Updated `tokio` to `1`. ### Added diff --git a/actix-web/Cargo.toml b/actix-web/Cargo.toml index 6e453026a..093c000b4 100644 --- a/actix-web/Cargo.toml +++ b/actix-web/Cargo.toml @@ -71,9 +71,9 @@ actix-service = "2" actix-utils = "3" actix-tls = { version = "3", default-features = false, optional = true } -actix-http = { version = "3.0.0", features = ["http2", "ws"] } -actix-router = "0.5.0" -actix-web-codegen = { version = "4.0.0", optional = true } +actix-http = { version = "3", features = ["http2", "ws"] } +actix-router = "0.5" +actix-web-codegen = { version = "4", optional = true } ahash = "0.7" bytes = "1" @@ -90,7 +90,7 @@ once_cell = "1.5" log = "0.4" mime = "0.3" pin-project-lite = "0.2.7" -regex = "1.4" +regex = "1.5.5" serde = "1.0" serde_json = "1.0" serde_urlencoded = "0.7" @@ -100,9 +100,9 @@ time = { version = "0.3", default-features = false, features = ["formatting"] } url = "2.1" [dev-dependencies] -actix-files = "0.6.0" +actix-files = "0.6" actix-test = { version = "0.1.0-beta.13", features = ["openssl", "rustls"] } -awc = { version = "3.0.0-beta.21", features = ["openssl"] } +awc = { version = "3", features = ["openssl"] } brotli = "3.3.3" const-str = "0.3" diff --git a/actix-web/README.md b/actix-web/README.md index d0abb3aae..957fb47b8 100644 --- a/actix-web/README.md +++ b/actix-web/README.md @@ -56,18 +56,19 @@ Code: ```rust use actix_web::{get, web, App, HttpServer, Responder}; -#[get("/{id}/{name}/index.html")] -async fn index(params: web::Path<(u32, String)>) -> impl Responder { - let (id, name) = params.into_inner(); - format!("Hello {}! id:{}", name, id) +#[get("/hello/{name}")] +async fn greet(name: web::Path) -> impl Responder { + format!("Hello {name}!") } #[actix_web::main] // or #[tokio::main] async fn main() -> std::io::Result<()> { - HttpServer::new(|| App::new().service(index)) - .bind(("127.0.0.1", 8080))? - .run() - .await + HttpServer::new(|| { + App::new().service(greet) + }) + .bind(("127.0.0.1", 8080))? + .run() + .await } ``` diff --git a/actix-web/src/lib.rs b/actix-web/src/lib.rs index 34bee7529..4eab24cec 100644 --- a/actix-web/src/lib.rs +++ b/actix-web/src/lib.rs @@ -4,18 +4,19 @@ //! ```no_run //! use actix_web::{get, web, App, HttpServer, Responder}; //! -//! #[get("/{id}/{name}/index.html")] -//! async fn index(path: web::Path<(u32, String)>) -> impl Responder { -//! let (id, name) = path.into_inner(); -//! format!("Hello {}! id:{}", name, id) +//! #[get("/hello/{name}")] +//! async fn greet(name: web::Path) -> impl Responder { +//! format!("Hello {}!", name) //! } //! -//! #[actix_web::main] +//! #[actix_web::main] // or #[tokio::main] //! async fn main() -> std::io::Result<()> { -//! HttpServer::new(|| App::new().service(index)) -//! .bind("127.0.0.1:8080")? -//! .run() -//! .await +//! HttpServer::new(|| { +//! App::new().service(greet) +//! }) +//! .bind(("127.0.0.1", 8080))? +//! .run() +//! .await //! } //! ``` //! diff --git a/actix-web/src/server.rs b/actix-web/src/server.rs index bdcfbf48a..99812600c 100644 --- a/actix-web/src/server.rs +++ b/actix-web/src/server.rs @@ -367,9 +367,7 @@ where .local_addr(addr); let svc = if let Some(handler) = on_connect_fn.clone() { - svc.on_connect_ext(move |io: &_, ext: _| { - (&*handler)(io as &dyn Any, ext) - }) + svc.on_connect_ext(move |io: &_, ext: _| (handler)(io as &dyn Any, ext)) } else { svc }; @@ -555,7 +553,7 @@ where if let Some(handler) = on_connect_fn.clone() { svc = svc - .on_connect_ext(move |io: &_, ext: _| (&*handler)(io as &dyn Any, ext)); + .on_connect_ext(move |io: &_, ext: _| (handler)(io as &dyn Any, ext)); } let fac = factory() diff --git a/actix-web/src/service.rs b/actix-web/src/service.rs index 3843abcf8..426e9d62b 100644 --- a/actix-web/src/service.rs +++ b/actix-web/src/service.rs @@ -78,18 +78,18 @@ pub struct ServiceRequest { } impl ServiceRequest { - /// Construct service request + /// Construct `ServiceRequest` from parts. pub(crate) fn new(req: HttpRequest, payload: Payload) -> Self { Self { req, payload } } - /// Deconstruct request into parts + /// Deconstruct `ServiceRequest` into inner parts. #[inline] pub fn into_parts(self) -> (HttpRequest, Payload) { (self.req, self.payload) } - /// Get mutable access to inner `HttpRequest` and `Payload` + /// Returns mutable accessors to inner parts. #[inline] pub fn parts_mut(&mut self) -> (&mut HttpRequest, &mut Payload) { (&mut self.req, &mut self.payload) @@ -105,9 +105,7 @@ impl ServiceRequest { Self { req, payload } } - /// Construct request from request. - /// - /// The returned `ServiceRequest` would have no payload. + /// Construct `ServiceRequest` with no payload from given `HttpRequest`. #[inline] pub fn from_request(req: HttpRequest) -> Self { ServiceRequest { @@ -116,63 +114,63 @@ impl ServiceRequest { } } - /// Create service response + /// Create `ServiceResponse` from this request and given response. #[inline] pub fn into_response>>(self, res: R) -> ServiceResponse { let res = HttpResponse::from(res.into()); ServiceResponse::new(self.req, res) } - /// Create service response for error + /// Create `ServiceResponse` from this request and given error. #[inline] pub fn error_response>(self, err: E) -> ServiceResponse { let res = HttpResponse::from_error(err.into()); ServiceResponse::new(self.req, res) } - /// This method returns reference to the request head + /// Returns a reference to the request head. #[inline] pub fn head(&self) -> &RequestHead { self.req.head() } - /// This method returns reference to the request head + /// Returns a mutable reference to the request head. #[inline] pub fn head_mut(&mut self) -> &mut RequestHead { self.req.head_mut() } - /// Request's uri. + /// Returns the request URI. #[inline] pub fn uri(&self) -> &Uri { &self.head().uri } - /// Read the Request method. + /// Returns the request method. #[inline] pub fn method(&self) -> &Method { &self.head().method } - /// Read the Request Version. + /// Returns the request version. #[inline] pub fn version(&self) -> Version { self.head().version } + /// Returns a reference to request headers. #[inline] - /// Returns request's headers. pub fn headers(&self) -> &HeaderMap { &self.head().headers } + /// Returns a mutable reference to request headers. #[inline] - /// Returns mutable request's headers. pub fn headers_mut(&mut self) -> &mut HeaderMap { &mut self.head_mut().headers } - /// The target path of this Request. + /// Returns request path. #[inline] pub fn path(&self) -> &str { self.head().uri.path() @@ -184,7 +182,7 @@ impl ServiceRequest { self.req.query_string() } - /// Peer socket address. + /// Returns peer's socket address. /// /// Peer address is the directly connected peer's socket address. If a proxy is used in front of /// the Actix Web server, then it would be address of this proxy. @@ -197,24 +195,23 @@ impl ServiceRequest { self.head().peer_addr } - /// Get *ConnectionInfo* for the current request. + /// Returns a reference to connection info. #[inline] pub fn connection_info(&self) -> Ref<'_, ConnectionInfo> { self.req.connection_info() } - /// Returns a reference to the Path parameters. + /// Returns reference to the Path parameters. /// - /// Params is a container for URL parameters. - /// A variable segment is specified in the form `{identifier}`, - /// where the identifier can be used later in a request handler to - /// access the matched value for that segment. + /// Params is a container for URL parameters. A variable segment is specified in the form + /// `{identifier}`, where the identifier can be used later in a request handler to access the + /// matched value for that segment. #[inline] pub fn match_info(&self) -> &Path { self.req.match_info() } - /// Returns a mutable reference to the Path parameters. + /// Returns a mutable reference to the path match information. #[inline] pub fn match_info_mut(&mut self) -> &mut Path { self.req.match_info_mut() @@ -232,13 +229,13 @@ impl ServiceRequest { self.req.match_pattern() } - /// Get a reference to a `ResourceMap` of current application. + /// Returns a reference to the application's resource map. #[inline] pub fn resource_map(&self) -> &ResourceMap { self.req.resource_map() } - /// Service configuration + /// Returns a reference to the application's configuration. #[inline] pub fn app_config(&self) -> &AppConfig { self.req.app_config() @@ -262,6 +259,7 @@ impl ServiceRequest { self.req.conn_data() } + /// Return request cookies. #[cfg(feature = "cookies")] #[inline] pub fn cookies(&self) -> Result>>, CookieParseError> { diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 3fd59512a..ebc0dbe61 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -3,6 +3,103 @@ ## Unreleased - 2021-xx-xx +## 3.0.0 - 2022-03-07 +### Dependencies +- Updated `actix-*` to Tokio v1-based versions. [#1813] +- Updated `bytes` to `1.0`. [#1813] +- Updated `cookie` to `0.16`. [#2555] +- Updated `rand` to `0.8`. +- Updated `rustls` to `0.20`. [#2414] +- Updated `tokio` to `1`. + +### Added +- `trust-dns` crate feature to enable `trust-dns-resolver` as client DNS resolver; disabled by default. [#1969] +- `cookies` crate feature; enabled by default. [#2619] +- `compress-brotli` crate feature; enabled by default. [#2250] +- `compress-gzip` crate feature; enabled by default. [#2250] +- `compress-zstd` crate feature; enabled by default. [#2250] +- `client::Connector::handshake_timeout()` for customizing TLS connection handshake timeout. [#2081] +- `client::ConnectorService` as `client::Connector::finish` method's return type [#2081] +- `client::ConnectionIo` trait alias [#2081] +- `Client::headers()` to get default mut reference of `HeaderMap` of client object. [#2114] +- `ClientResponse::timeout()` for set the timeout of collecting response body. [#1931] +- `ClientBuilder::local_address()` for binding to a local IP address for this client. [#2024] +- `ClientRequest::insert_header()` method which allows using typed and untyped headers. [#1869] +- `ClientRequest::append_header()` method which allows using typed and untyped headers. [#1869] +- `ClientBuilder::add_default_header()` (and deprecate `ClientBuilder::header()`). [#2510] + +### Changed +- `client::Connector` type now only has one generic type for `actix_service::Service`. [#2063] +- `client::error::ConnectError` Resolver variant contains `Box` type. [#1905] +- `client::ConnectorConfig` default timeout changed to 5 seconds. [#1905] +- `ConnectorService` type is renamed to `BoxConnectorService`. [#2081] +- Fix http/https encoding when enabling `compress` feature. [#2116] +- Rename `TestResponse::{header => append_header, set => insert_header}`. These methods now take a `TryIntoHeaderPair`. [#2094] +- `ClientBuilder::connector()` method now takes `Connector` type. [#2008] +- Basic auth now accepts blank passwords as an empty string instead of an `Option`. [#2050] +- Relax default timeout for `Connector` to 5 seconds (up from 1 second). [#1905] +- `*::send_json()` and `*::send_form()` methods now receive `impl Serialize`. [#2553] +- `FrozenClientRequest::extra_header()` now uses receives an `impl TryIntoHeaderPair`. [#2553] +- Rename `Connector::{ssl => openssl}()`. [#2503] +- `ClientRequest::send_body` now takes an `impl MessageBody`. [#2546] +- Rename `MessageBody => ResponseBody` to avoid conflicts with `MessageBody` trait. [#2546] +- Minimum supported Rust version (MSRV) is now 1.54. + +### Fixed +- Send headers along with redirected requests. [#2310] +- Improve `Client` instantiation efficiency when using `openssl` by only building connectors once. [#2503] +- Remove unnecessary `Unpin` bounds on `*::send_stream`. [#2553] +- `impl Future` for `ResponseBody` no longer requires the body type be `Unpin`. [#2546] +- `impl Future` for `JsonBody` no longer requires the body type be `Unpin`. [#2546] +- `impl Stream` for `ClientResponse` no longer requires the body type be `Unpin`. [#2546] + +### Removed +- `compress` crate feature. [#2250] +- `ClientRequest::set`; use `ClientRequest::insert_header`. [#1869] +- `ClientRequest::set_header`; use `ClientRequest::insert_header`. [#1869] +- `ClientRequest::set_header_if_none`; use `ClientRequest::insert_header_if_none`. [#1869] +- `ClientRequest::header`; use `ClientRequest::append_header`. [#1869] +- Deprecated methods on `ClientRequest`: `if_true`, `if_some`. [#2148] +- `ClientBuilder::default` function [#2008] + +### Security +- `cookie` upgrade addresses [`RUSTSEC-2020-0071`]. + +[`RUSTSEC-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html + +[#1813]: https://github.com/actix/actix-web/pull/1813 +[#1869]: https://github.com/actix/actix-web/pull/1869 +[#1905]: https://github.com/actix/actix-web/pull/1905 +[#1905]: https://github.com/actix/actix-web/pull/1905 +[#1931]: https://github.com/actix/actix-web/pull/1931 +[#1969]: https://github.com/actix/actix-web/pull/1969 +[#1969]: https://github.com/actix/actix-web/pull/1969 +[#1981]: https://github.com/actix/actix-web/pull/1981 +[#2008]: https://github.com/actix/actix-web/pull/2008 +[#2024]: https://github.com/actix/actix-web/pull/2024 +[#2050]: https://github.com/actix/actix-web/pull/2050 +[#2063]: https://github.com/actix/actix-web/pull/2063 +[#2081]: https://github.com/actix/actix-web/pull/2081 +[#2081]: https://github.com/actix/actix-web/pull/2081 +[#2094]: https://github.com/actix/actix-web/pull/2094 +[#2114]: https://github.com/actix/actix-web/pull/2114 +[#2116]: https://github.com/actix/actix-web/pull/2116 +[#2148]: https://github.com/actix/actix-web/pull/2148 +[#2250]: https://github.com/actix/actix-web/pull/2250 +[#2310]: https://github.com/actix/actix-web/pull/2310 +[#2414]: https://github.com/actix/actix-web/pull/2414 +[#2425]: https://github.com/actix/actix-web/pull/2425 +[#2474]: https://github.com/actix/actix-web/pull/2474 +[#2503]: https://github.com/actix/actix-web/pull/2503 +[#2510]: https://github.com/actix/actix-web/pull/2510 +[#2546]: https://github.com/actix/actix-web/pull/2546 +[#2553]: https://github.com/actix/actix-web/pull/2553 +[#2555]: https://github.com/actix/actix-web/pull/2555 + + +
+3.0.0 Pre-Releases + ## 3.0.0-beta.21 - 2022-02-16 - No significant changes since `3.0.0-beta.20`. @@ -170,6 +267,7 @@ [#1813]: https://github.com/actix/actix-web/pull/1813 +
## 2.0.3 - 2020-11-29 ### Fixed diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 40d9d34b6..9dd29e4b7 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "awc" -version = "3.0.0-beta.21" +version = "3.0.0" authors = [ "Nikolay Kim ", "fakeshadow <24548779@qq.com>", ] -description = "Async HTTP and WebSocket client library built on the Actix ecosystem" +description = "Async HTTP and WebSocket client library" keywords = ["actix", "http", "framework", "async", "web"] categories = [ "network-programming", @@ -59,11 +59,11 @@ dangerous-h2c = [] [dependencies] actix-codec = "0.5" -actix-service = "2.0.0" -actix-http = { version = "3.0.0", features = ["http2", "ws"] } +actix-service = "2" +actix-http = { version = "3", features = ["http2", "ws"] } actix-rt = { version = "2.1", default-features = false } actix-tls = { version = "3", features = ["connect", "uri"] } -actix-utils = "3.0.0" +actix-utils = "3" ahash = "0.7" base64 = "0.13" @@ -93,13 +93,13 @@ tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features trust-dns-resolver = { version = "0.20.0", optional = true } [dev-dependencies] -actix-http = { version = "3.0.0", features = ["openssl"] } +actix-http = { version = "3", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.13", features = ["openssl"] } actix-server = "2" actix-test = { version = "0.1.0-beta.13", features = ["openssl", "rustls"] } actix-tls = { version = "3", features = ["openssl", "rustls"] } -actix-utils = "3.0.0" -actix-web = { version = "4.0.0", features = ["openssl"] } +actix-utils = "3" +actix-web = { version = "4", features = ["openssl"] } brotli = "3.3.3" const-str = "0.3" diff --git a/awc/README.md b/awc/README.md index 417647e62..db70f7332 100644 --- a/awc/README.md +++ b/awc/README.md @@ -3,9 +3,9 @@ > Async HTTP and WebSocket client library. [![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc) -[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.21)](https://docs.rs/awc/3.0.0-beta.21) +[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0)](https://docs.rs/awc/3.0.0) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) -[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.21/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.21) +[![Dependency Status](https://deps.rs/crate/awc/3.0.0/status.svg)](https://deps.rs/crate/awc/3.0.0) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources diff --git a/awc/src/client/connector.rs b/awc/src/client/connector.rs index 26c62b924..51d6e180b 100644 --- a/awc/src/client/connector.rs +++ b/awc/src/client/connector.rs @@ -246,7 +246,12 @@ where /// /// The default limit size is 100. pub fn limit(mut self, limit: usize) -> Self { - self.config.limit = limit; + if limit == 0 { + self.config.limit = u32::MAX as usize; + } else { + self.config.limit = limit; + } + self } diff --git a/awc/src/client/h1proto.rs b/awc/src/client/h1proto.rs index 4f6a87ac5..8738c2f7f 100644 --- a/awc/src/client/h1proto.rs +++ b/awc/src/client/h1proto.rs @@ -83,12 +83,12 @@ where false }; - framed.send((head, body.size()).into()).await?; - let mut pin_framed = Pin::new(&mut framed); // special handle for EXPECT request. let (do_send, mut res_head) = if is_expect { + pin_framed.send((head, body.size()).into()).await?; + let head = poll_fn(|cx| pin_framed.as_mut().poll_next(cx)) .await .ok_or(ConnectError::Disconnected)??; @@ -97,13 +97,17 @@ where // and current head would be used as final response head. (head.status == StatusCode::CONTINUE, Some(head)) } else { + pin_framed.feed((head, body.size()).into()).await?; + (true, None) }; if do_send { // send request body match body.size() { - BodySize::None | BodySize::Sized(0) => {} + BodySize::None | BodySize::Sized(0) => { + poll_fn(|cx| pin_framed.as_mut().flush(cx)).await?; + } _ => send_body(body, pin_framed.as_mut()).await?, }; diff --git a/awc/src/connect.rs b/awc/src/connect.rs index f93014a67..be1ea0fee 100644 --- a/awc/src/connect.rs +++ b/awc/src/connect.rs @@ -30,17 +30,35 @@ pub type BoxConnectorService = Rc< pub type BoxedSocket = Box; +/// Combined HTTP and WebSocket request type received by connection service. pub enum ConnectRequest { + /// Standard HTTP request. + /// + /// Contains the request head, body type, and optional pre-resolved socket address. Client(RequestHeadType, AnyBody, Option), + + /// Tunnel used by WebSocket connection requests. + /// + /// Contains the request head and optional pre-resolved socket address. Tunnel(RequestHead, Option), } +/// Combined HTTP response & WebSocket tunnel type returned from connection service. pub enum ConnectResponse { + /// Standard HTTP response. Client(ClientResponse), + + /// Tunnel used for WebSocket communication. + /// + /// Contains response head and framed HTTP/1.1 codec. Tunnel(ResponseHead, Framed), } impl ConnectResponse { + /// Unwraps type into HTTP response. + /// + /// # Panics + /// Panics if enum variant is not `Client`. pub fn into_client_response(self) -> ClientResponse { match self { ConnectResponse::Client(res) => res, @@ -50,6 +68,10 @@ impl ConnectResponse { } } + /// Unwraps type into WebSocket tunnel response. + /// + /// # Panics + /// Panics if enum variant is not `Tunnel`. pub fn into_tunnel_response(self) -> (ResponseHead, Framed) { match self { ConnectResponse::Tunnel(head, framed) => (head, framed), @@ -136,30 +158,37 @@ where ConnectRequestProj::Connection { fut, req } => { let connection = ready!(fut.poll(cx))?; let req = req.take().unwrap(); + match req { ConnectRequest::Client(head, body, ..) => { // send request let fut = ConnectRequestFuture::Client { fut: connection.send_request(head, body), }; + self.set(fut); } + ConnectRequest::Tunnel(head, ..) => { // send request let fut = ConnectRequestFuture::Tunnel { fut: connection.open_tunnel(RequestHeadType::from(head)), }; + self.set(fut); } } + self.poll(cx) } + ConnectRequestProj::Client { fut } => { let (head, payload) = ready!(fut.as_mut().poll(cx))?; Poll::Ready(Ok(ConnectResponse::Client(ClientResponse::new( head, payload, )))) } + ConnectRequestProj::Tunnel { fut } => { let (head, framed) = ready!(fut.as_mut().poll(cx))?; let framed = framed.into_map_io(|io| Box::new(io) as _); diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 970ca2d92..8d6ea759a 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -1,22 +1,25 @@ -//! `awc` is a HTTP and WebSocket client library built on the Actix ecosystem. +//! `awc` is an asynchronous HTTP and WebSocket client library. //! -//! # Making a GET request +//! # `GET` Requests //! ```no_run //! # #[actix_rt::main] //! # async fn main() -> Result<(), awc::error::SendRequestError> { +//! // create client //! let mut client = awc::Client::default(); -//! let response = client.get("http://www.rust-lang.org") // <- Create request builder -//! .insert_header(("User-Agent", "Actix-web")) -//! .send() // <- Send http request -//! .await?; //! -//! println!("Response: {:?}", response); +//! // construct request +//! let req = client.get("http://www.rust-lang.org") +//! .insert_header(("User-Agent", "awc/3.0")); +//! +//! // send request and await response +//! let res = req.send().await?; +//! println!("Response: {:?}", res); //! # Ok(()) //! # } //! ``` //! -//! # Making POST requests -//! ## Raw body contents +//! # `POST` Requests +//! ## Raw Body //! ```no_run //! # #[actix_rt::main] //! # async fn main() -> Result<(), awc::error::SendRequestError> { @@ -28,20 +31,6 @@ //! # } //! ``` //! -//! ## Forms -//! ```no_run -//! # #[actix_rt::main] -//! # async fn main() -> Result<(), awc::error::SendRequestError> { -//! let params = [("foo", "bar"), ("baz", "quux")]; -//! -//! let mut client = awc::Client::default(); -//! let response = client.post("http://httpbin.org/post") -//! .send_form(¶ms) -//! .await?; -//! # Ok(()) -//! # } -//! ``` -//! //! ## JSON //! ```no_run //! # #[actix_rt::main] @@ -59,6 +48,20 @@ //! # } //! ``` //! +//! ## URL Encoded Form +//! ```no_run +//! # #[actix_rt::main] +//! # async fn main() -> Result<(), awc::error::SendRequestError> { +//! let params = [("foo", "bar"), ("baz", "quux")]; +//! +//! let mut client = awc::Client::default(); +//! let response = client.post("http://httpbin.org/post") +//! .send_form(¶ms) +//! .await?; +//! # Ok(()) +//! # } +//! ``` +//! //! # Response Compression //! All [official][iana-encodings] and common content encoding codecs are supported, optionally. //! @@ -76,11 +79,12 @@ //! //! [iana-encodings]: https://www.iana.org/assignments/http-parameters/http-parameters.xhtml#content-coding //! -//! # WebSocket support +//! # WebSockets //! ```no_run //! # #[actix_rt::main] //! # async fn main() -> Result<(), Box> { -//! use futures_util::{sink::SinkExt, stream::StreamExt}; +//! use futures_util::{sink::SinkExt as _, stream::StreamExt as _}; +//! //! let (_resp, mut connection) = awc::Client::new() //! .ws("ws://echo.websocket.org") //! .connect() @@ -89,8 +93,9 @@ //! connection //! .send(awc::ws::Message::Text("Echo".into())) //! .await?; +//! //! let response = connection.next().await.unwrap()?; -//! # assert_eq!(response, awc::ws::Frame::Text("Echo".as_bytes().into())); +//! assert_eq!(response, awc::ws::Frame::Text("Echo".into())); //! # Ok(()) //! # } //! ``` diff --git a/awc/src/middleware/redirect.rs b/awc/src/middleware/redirect.rs index ac6690471..d48822168 100644 --- a/awc/src/middleware/redirect.rs +++ b/awc/src/middleware/redirect.rs @@ -161,7 +161,8 @@ where | StatusCode::SEE_OTHER | StatusCode::TEMPORARY_REDIRECT | StatusCode::PERMANENT_REDIRECT - if *max_redirect_times > 0 => + if *max_redirect_times > 0 + && res.headers().contains_key(header::LOCATION) => { let reuse_body = res.head().status == StatusCode::TEMPORARY_REDIRECT || res.head().status == StatusCode::PERMANENT_REDIRECT; @@ -245,26 +246,32 @@ where } fn build_next_uri(res: &ClientResponse, prev_uri: &Uri) -> Result { - let uri = res - .headers() - .get(header::LOCATION) - .map(|value| { - // try to parse the location to a full uri - let uri = Uri::try_from(value.as_bytes()) - .map_err(|e| SendRequestError::Url(InvalidUrl::HttpError(e.into())))?; - if uri.scheme().is_none() || uri.authority().is_none() { - let uri = Uri::builder() - .scheme(prev_uri.scheme().cloned().unwrap()) - .authority(prev_uri.authority().cloned().unwrap()) - .path_and_query(value.as_bytes()) - .build()?; - Ok::<_, SendRequestError>(uri) - } else { - Ok(uri) - } - }) - // TODO: this error type is wrong. - .ok_or(SendRequestError::Url(InvalidUrl::MissingScheme))??; + // responses without this header are not processed + let location = res.headers().get(header::LOCATION).unwrap(); + + // try to parse the location and resolve to a full URI but fall back to default if it fails + let uri = Uri::try_from(location.as_bytes()).unwrap_or_else(|_| Uri::default()); + + let uri = if uri.scheme().is_none() || uri.authority().is_none() { + let builder = Uri::builder() + .scheme(prev_uri.scheme().cloned().unwrap()) + .authority(prev_uri.authority().cloned().unwrap()); + + // when scheme or authority is missing treat the location value as path and query + // recover error where location does not have leading slash + let path = if location.as_bytes().starts_with(b"/") { + location.as_bytes().to_owned() + } else { + [b"/", location.as_bytes()].concat() + }; + + builder + .path_and_query(path) + .build() + .map_err(|err| SendRequestError::Url(InvalidUrl::HttpError(err)))? + } else { + uri + }; Ok(uri) } @@ -287,10 +294,13 @@ mod tests { use actix_web::{web, App, Error, HttpRequest, HttpResponse}; use super::*; - use crate::{http::header::HeaderValue, ClientBuilder}; + use crate::{ + http::{header::HeaderValue, StatusCode}, + ClientBuilder, + }; #[actix_rt::test] - async fn test_basic_redirect() { + async fn basic_redirect() { let client = ClientBuilder::new() .disable_redirects() .wrap(Redirect::new().max_redirect_times(10)) @@ -315,6 +325,44 @@ mod tests { assert_eq!(res.status().as_u16(), 400); } + #[actix_rt::test] + async fn redirect_relative_without_leading_slash() { + let client = ClientBuilder::new().finish(); + + let srv = actix_test::start(|| { + App::new() + .service(web::resource("/").route(web::to(|| async { + HttpResponse::Found() + .insert_header(("location", "abc/")) + .finish() + }))) + .service( + web::resource("/abc/") + .route(web::to(|| async { HttpResponse::Accepted().finish() })), + ) + }); + + let res = client.get(srv.url("/")).send().await.unwrap(); + assert_eq!(res.status(), StatusCode::ACCEPTED); + } + + #[actix_rt::test] + async fn redirect_without_location() { + let client = ClientBuilder::new() + .disable_redirects() + .wrap(Redirect::new().max_redirect_times(10)) + .finish(); + + let srv = actix_test::start(|| { + App::new().service(web::resource("/").route(web::to(|| async { + Ok::<_, Error>(HttpResponse::Found().finish()) + }))) + }); + + let res = client.get(srv.url("/")).send().await.unwrap(); + assert_eq!(res.status(), StatusCode::FOUND); + } + #[actix_rt::test] async fn test_redirect_limit() { let client = ClientBuilder::new() @@ -328,14 +376,14 @@ mod tests { .service(web::resource("/").route(web::to(|| async { Ok::<_, Error>( HttpResponse::Found() - .append_header(("location", "/test")) + .insert_header(("location", "/test")) .finish(), ) }))) .service(web::resource("/test").route(web::to(|| async { Ok::<_, Error>( HttpResponse::Found() - .append_header(("location", "/test2")) + .insert_header(("location", "/test2")) .finish(), ) }))) @@ -345,8 +393,15 @@ mod tests { }); let res = client.get(srv.url("/")).send().await.unwrap(); - - assert_eq!(res.status().as_u16(), 302); + assert_eq!(res.status(), StatusCode::FOUND); + assert_eq!( + res.headers() + .get(header::LOCATION) + .unwrap() + .to_str() + .unwrap(), + "/test2" + ); } #[actix_rt::test] diff --git a/awc/src/request.rs b/awc/src/request.rs index 8bcf1ee01..102db3c16 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -505,7 +505,7 @@ impl fmt::Debug for ClientRequest { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!( f, - "\nClientRequest {:?} {}:{}", + "\nClientRequest {:?} {} {}", self.head.version, self.head.method, self.head.uri )?; writeln!(f, " headers:")?; diff --git a/awc/src/responses/response.rs b/awc/src/responses/response.rs index 4e6a05f0f..c7c0a6362 100644 --- a/awc/src/responses/response.rs +++ b/awc/src/responses/response.rs @@ -160,7 +160,7 @@ where /// /// # Errors /// `Future` implementation returns error if: - /// - content length is greater than [limit](JsonBody::limit) (default: 2 MiB) + /// - content length is greater than [limit](ResponseBody::limit) (default: 2 MiB) /// /// # Examples /// ```no_run