Merge branch 'master' into logger-custom-response-replace

This commit is contained in:
Benny Nazimov 2022-03-20 22:42:58 +02:00 committed by GitHub
commit 1fd700e85b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 626 additions and 290 deletions

View File

@ -78,15 +78,6 @@ jobs:
cargo test --lib --tests -p=actix-multipart --all-features cargo test --lib --tests -p=actix-multipart --all-features
cargo test --lib --tests -p=actix-web-actors --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 - name: Clear the cargo caches
run: | run: |
cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean
@ -125,34 +116,6 @@ jobs:
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: { command: ci-check-all-feature-powerset-linux } 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: nextest:
name: nextest name: nextest
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@ -81,19 +81,37 @@ jobs:
cargo test --lib --tests -p=actix-multipart --all-features cargo test --lib --tests -p=actix-multipart --all-features
cargo test --lib --tests -p=actix-web-actors --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) - name: tests (io-uring)
if: matrix.target.os == 'ubuntu-latest'
timeout-minutes: 60 timeout-minutes: 60
run: > run: >
sudo bash -c "ulimit -Sl 512 sudo bash -c "ulimit -Sl 512
&& ulimit -Hl 512 && ulimit -Hl 512
&& PATH=$PATH:/usr/share/rust/.cargo/bin && PATH=$PATH:/usr/share/rust/.cargo/bin
&& RUSTUP_TOOLCHAIN=${{ matrix.version }} cargo test --lib --tests -p=actix-files --all-features" && RUSTUP_TOOLCHAIN=stable 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
rustdoc: rustdoc:
name: doc tests name: doc tests

36
.github/workflows/coverage.yml vendored Normal file
View File

@ -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 }

View File

@ -1,8 +1,11 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## 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 [#2645]: https://github.com/actix/actix-web/pull/2645

View File

@ -39,10 +39,12 @@ mime_guess = "2.0.1"
percent-encoding = "2.1" percent-encoding = "2.1"
pin-project-lite = "0.2.7" pin-project-lite = "0.2.7"
# experimental-io-uring
tokio-uring = { version = "0.2", optional = true, features = ["bytes"] } tokio-uring = { version = "0.2", optional = true, features = ["bytes"] }
actix-server = "2.1" # ensure matching tokio-uring versions
[dev-dependencies] [dev-dependencies]
actix-rt = "2.2" actix-rt = "2.7"
actix-test = "0.1.0-beta.13" actix-test = "0.1.0-beta.13"
actix-web = "4.0.0" actix-web = "4.0.0"
tempfile = "3.2" tempfile = "3.2"

View File

@ -364,20 +364,43 @@ mod tests {
); );
} }
#[allow(deprecated)]
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_status_code_text() { async fn status_code_customize_same_output() {
let mut file = NamedFile::open_async("Cargo.toml") let file1 = NamedFile::open_async("Cargo.toml")
.await .await
.unwrap() .unwrap()
.set_status_code(StatusCode::NOT_FOUND); .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(); file.file();
let _f: &File = &file; let _f: &File = &file;
} }
{ {
let _f: &mut File = &mut file; let _f: &mut File = &mut file;
} }
let file = file.customize().with_status(StatusCode::NOT_FOUND);
let req = TestRequest::default().to_http_request(); let req = TestRequest::default().to_http_request();
let resp = file.respond_to(&req); let resp = file.respond_to(&req);
assert_eq!( assert_eq!(

View File

@ -23,6 +23,7 @@ use actix_web::{
use bitflags::bitflags; use bitflags::bitflags;
use derive_more::{Deref, DerefMut}; use derive_more::{Deref, DerefMut};
use futures_core::future::LocalBoxFuture; use futures_core::future::LocalBoxFuture;
use mime::Mime;
use mime_guess::from_path; use mime_guess::from_path;
use crate::{encoding::equiv_utf8_text, range::HttpRange}; use crate::{encoding::equiv_utf8_text, range::HttpRange};
@ -76,8 +77,8 @@ pub struct NamedFile {
pub(crate) md: Metadata, pub(crate) md: Metadata,
pub(crate) flags: Flags, pub(crate) flags: Flags,
pub(crate) status_code: StatusCode, pub(crate) status_code: StatusCode,
pub(crate) content_type: mime::Mime, pub(crate) content_type: Mime,
pub(crate) content_disposition: header::ContentDisposition, pub(crate) content_disposition: ContentDisposition,
pub(crate) encoding: Option<ContentEncoding>, pub(crate) encoding: Option<ContentEncoding>,
} }
@ -209,11 +210,10 @@ impl NamedFile {
Self::from_file(file, path) Self::from_file(file, path)
} }
#[allow(rustdoc::broken_intra_doc_links)]
/// Attempts to open a file asynchronously in read-only mode. /// Attempts to open a file asynchronously in read-only mode.
/// ///
/// When the `experimental-io-uring` crate feature is enabled, this will be async. /// When the `experimental-io-uring` crate feature is enabled, this will be async. Otherwise, it
/// Otherwise, it will be just like [`open`][Self::open]. /// will behave just like `open`.
/// ///
/// # Examples /// # Examples
/// ``` /// ```
@ -238,13 +238,13 @@ impl NamedFile {
Self::from_file(file, path) Self::from_file(file, path)
} }
/// Returns reference to the underlying `File` object. /// Returns reference to the underlying file object.
#[inline] #[inline]
pub fn file(&self) -> &File { pub fn file(&self) -> &File {
&self.file &self.file
} }
/// Retrieve the path of this file. /// Returns the filesystem path to this file.
/// ///
/// # Examples /// # Examples
/// ``` /// ```
@ -262,16 +262,53 @@ impl NamedFile {
self.path.as_path() 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<SystemTime> {
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<ContentEncoding> {
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 { pub fn set_status_code(mut self, status: StatusCode) -> Self {
self.status_code = status; self.status_code = status;
self self
} }
/// Set the MIME Content-Type for serving this file. By default the Content-Type is inferred /// Sets the `Content-Type` header that will be used when serving this file. By default the
/// from the filename extension. /// `Content-Type` is inferred from the filename extension.
#[inline] #[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.content_type = mime_type;
self self
} }
@ -284,15 +321,15 @@ impl NamedFile {
/// filename is taken from the path provided in the `open` method after converting it to UTF-8 /// filename is taken from the path provided in the `open` method after converting it to UTF-8
/// (using `to_string_lossy`). /// (using `to_string_lossy`).
#[inline] #[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.content_disposition = cd;
self.flags.insert(Flags::CONTENT_DISPOSITION); self.flags.insert(Flags::CONTENT_DISPOSITION);
self 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] #[inline]
pub fn disable_content_disposition(mut self) -> Self { pub fn disable_content_disposition(mut self) -> Self {
self.flags.remove(Flags::CONTENT_DISPOSITION); self.flags.remove(Flags::CONTENT_DISPOSITION);

View File

@ -29,13 +29,13 @@ default = []
openssl = ["tls-openssl", "awc/openssl"] openssl = ["tls-openssl", "awc/openssl"]
[dependencies] [dependencies]
actix-service = "2.0.0" actix-service = "2"
actix-codec = "0.5" actix-codec = "0.5"
actix-tls = "3" actix-tls = "3"
actix-utils = "3.0.0" actix-utils = "3"
actix-rt = "2.2" actix-rt = "2.2"
actix-server = "2" actix-server = "2"
awc = { version = "3.0.0-beta.21", default-features = false } awc = { version = "3", default-features = false }
base64 = "0.13" base64 = "0.13"
bytes = "1" bytes = "1"
@ -51,5 +51,5 @@ tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
tokio = { version = "1.8.4", features = ["sync"] } tokio = { version = "1.8.4", features = ["sync"] }
[dev-dependencies] [dev-dependencies]
actix-web = { version = "4.0.0", default-features = false, features = ["cookies"] } actix-web = { version = "4", default-features = false, features = ["cookies"] }
actix-http = "3.0.0" actix-http = "3"

View File

@ -3,6 +3,30 @@
## Unreleased - 2021-xx-xx ## 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 ## 3.0.0 - 2022-02-25
### Dependencies ### Dependencies
- Updated `actix-*` to Tokio v1-based versions. [#1813] - Updated `actix-*` to Tokio v1-based versions. [#1813]
@ -745,10 +769,10 @@
- Remove `ResponseError` impl for `actix::actors::resolver::ResolverError` - Remove `ResponseError` impl for `actix::actors::resolver::ResolverError`
due to deprecate of resolver actor. [#1813] due to deprecate of resolver actor. [#1813]
- Remove `ConnectError::SslHandshakeError` and re-export of `HandshakeError`. - 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] error would return as `ConnectError::SslError`. [#1813]
- Remove `actix-threadpool` dependency. Use `actix_rt::task::spawn_blocking`. - 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] `actix_http::error` module. [#1878]
[#1813]: https://github.com/actix/actix-web/pull/1813 [#1813]: https://github.com/actix/actix-web/pull/1813

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-http" name = "actix-http"
version = "3.0.0" version = "3.0.4"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>", "Rob Ede <robjtede@icloud.com>",
@ -20,7 +20,7 @@ edition = "2018"
[package.metadata.docs.rs] [package.metadata.docs.rs]
# features that docs.rs will build with # 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] [lib]
name = "actix_http" name = "actix_http"
@ -73,11 +73,11 @@ httparse = "1.5.1"
httpdate = "1.0.1" httpdate = "1.0.1"
itoa = "1" itoa = "1"
language-tags = "0.3" language-tags = "0.3"
log = "0.4"
mime = "0.3" mime = "0.3"
percent-encoding = "2.1" percent-encoding = "2.1"
pin-project-lite = "0.2" pin-project-lite = "0.2"
smallvec = "1.6.1" smallvec = "1.6.1"
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
# http2 # http2
h2 = { version = "0.3.9", optional = true } h2 = { version = "0.3.9", optional = true }
@ -110,6 +110,7 @@ memchr = "2.4"
once_cell = "1.9" once_cell = "1.9"
rcgen = "0.8" rcgen = "0.8"
regex = "1.3" regex = "1.3"
rustversion = "1"
rustls-pemfile = "0.2" rustls-pemfile = "0.2"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
@ -120,7 +121,7 @@ tokio = { version = "1.8.4", features = ["net", "rt", "macros"] }
[[example]] [[example]]
name = "ws" name = "ws"
required-features = ["rustls"] required-features = ["ws", "rustls"]
[[bench]] [[bench]]
name = "write-camel-case" name = "write-camel-case"

View File

@ -3,11 +3,11 @@
> HTTP primitives for the Actix ecosystem. > HTTP primitives for the Actix ecosystem.
[![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) [![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) [![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) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
<br /> <br />
[![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) [![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) [![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 actix_server::Server;
use futures_util::future; use futures_util::future;
use http::header::HeaderValue; use http::header::HeaderValue;
use log::info; use tracing::info;
#[actix_rt::main] #[actix_rt::main]
async fn main() -> io::Result<()> { async fn main() -> io::Result<()> {

View File

@ -18,7 +18,8 @@ async fn main() -> std::io::Result<()> {
HttpService::build() HttpService::build()
// pass the app to service builder // pass the app to service builder
// map_config is used to map App's configuration to ServiceBuilder // 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() .tcp()
})? })?
.run() .run()

View File

@ -5,6 +5,7 @@ use actix_server::Server;
use bytes::BytesMut; use bytes::BytesMut;
use futures_util::StreamExt as _; use futures_util::StreamExt as _;
use http::header::HeaderValue; use http::header::HeaderValue;
use tracing::info;
#[actix_rt::main] #[actix_rt::main]
async fn main() -> io::Result<()> { async fn main() -> io::Result<()> {
@ -22,7 +23,7 @@ async fn main() -> io::Result<()> {
body.extend_from_slice(&item?); body.extend_from_slice(&item?);
} }
log::info!("request body: {:?}", body); info!("request body: {:?}", body);
let res = Response::build(StatusCode::OK) let res = Response::build(StatusCode::OK)
.insert_header(("x-head", HeaderValue::from_static("dummy value!"))) .insert_header(("x-head", HeaderValue::from_static("dummy value!")))

View File

@ -1,9 +1,8 @@
use std::{convert::Infallible, io, time::Duration}; use std::{convert::Infallible, io, time::Duration};
use actix_http::{ use actix_http::{header::HeaderValue, HttpService, Request, Response, StatusCode};
header::HeaderValue, HttpMessage, HttpService, Request, Response, StatusCode,
};
use actix_server::Server; use actix_server::Server;
use tracing::info;
#[actix_rt::main] #[actix_rt::main]
async fn main() -> io::Result<()> { async fn main() -> io::Result<()> {
@ -18,12 +17,12 @@ async fn main() -> io::Result<()> {
ext.insert(42u32); ext.insert(42u32);
}) })
.finish(|req: Request| async move { .finish(|req: Request| async move {
log::info!("{:?}", req); info!("{:?}", req);
let mut res = Response::build(StatusCode::OK); let mut res = Response::build(StatusCode::OK);
res.insert_header(("x-head", HeaderValue::from_static("dummy value!"))); res.insert_header(("x-head", HeaderValue::from_static("dummy value!")));
let forty_two = req.extensions().get::<u32>().unwrap().to_string(); let forty_two = req.conn_data::<u32>().unwrap().to_string();
res.insert_header(( res.insert_header((
"x-forty-two", "x-forty-two",
HeaderValue::from_str(&forty_two).unwrap(), HeaderValue::from_str(&forty_two).unwrap(),

View File

@ -12,6 +12,7 @@ use actix_http::{body::BodyStream, HttpService, Response};
use actix_server::Server; use actix_server::Server;
use async_stream::stream; use async_stream::stream;
use bytes::Bytes; use bytes::Bytes;
use tracing::info;
#[actix_rt::main] #[actix_rt::main]
async fn main() -> io::Result<()> { async fn main() -> io::Result<()> {
@ -21,7 +22,7 @@ async fn main() -> io::Result<()> {
.bind("streaming-error", ("127.0.0.1", 8080), || { .bind("streaming-error", ("127.0.0.1", 8080), || {
HttpService::build() HttpService::build()
.finish(|req| async move { .finish(|req| async move {
log::info!("{:?}", req); info!("{:?}", req);
let res = Response::ok(); let res = Response::ok();
Ok::<_, Infallible>(res.set_body(BodyStream::new(stream! { Ok::<_, Infallible>(res.set_body(BodyStream::new(stream! {

View File

@ -17,6 +17,7 @@ use actix_server::Server;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use bytestring::ByteString; use bytestring::ByteString;
use futures_core::{ready, Stream}; use futures_core::{ready, Stream};
use tracing::{info, trace};
#[actix_rt::main] #[actix_rt::main]
async fn main() -> io::Result<()> { async fn main() -> io::Result<()> {
@ -34,13 +35,13 @@ async fn main() -> io::Result<()> {
} }
async fn handler(req: Request) -> Result<Response<BodyStream<Heartbeat>>, Error> { async fn handler(req: Request) -> Result<Response<BodyStream<Heartbeat>>, Error> {
log::info!("handshaking"); info!("handshaking");
let mut res = ws::handshake(req.head())?; let mut res = ws::handshake(req.head())?;
// handshake will always fail under HTTP/2 // handshake will always fail under HTTP/2
log::info!("responding"); info!("responding");
Ok(res.message_body(BodyStream::new(Heartbeat::new(ws::Codec::new())))?) res.message_body(BodyStream::new(Heartbeat::new(ws::Codec::new())))
} }
struct Heartbeat { struct Heartbeat {
@ -61,7 +62,7 @@ impl Stream for Heartbeat {
type Item = Result<Bytes, Error>; type Item = Result<Bytes, Error>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
log::trace!("poll"); trace!("poll");
ready!(self.as_mut().interval.poll_tick(cx)); ready!(self.as_mut().interval.poll_tick(cx));

View File

@ -17,6 +17,7 @@ use pin_project_lite::pin_project;
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
use flate2::write::{GzEncoder, ZlibEncoder}; use flate2::write::{GzEncoder, ZlibEncoder};
use tracing::trace;
#[cfg(feature = "compress-zstd")] #[cfg(feature = "compress-zstd")]
use zstd::stream::write::Encoder as ZstdEncoder; use zstd::stream::write::Encoder as ZstdEncoder;
@ -356,7 +357,7 @@ impl ContentEncoder {
ContentEncoder::Brotli(ref mut encoder) => match encoder.write_all(data) { ContentEncoder::Brotli(ref mut encoder) => match encoder.write_all(data) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => { Err(err) => {
log::trace!("Error decoding br encoding: {}", err); trace!("Error decoding br encoding: {}", err);
Err(err) Err(err)
} }
}, },
@ -365,7 +366,7 @@ impl ContentEncoder {
ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) { ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => { Err(err) => {
log::trace!("Error decoding gzip encoding: {}", err); trace!("Error decoding gzip encoding: {}", err);
Err(err) Err(err)
} }
}, },
@ -374,7 +375,7 @@ impl ContentEncoder {
ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) { ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => { Err(err) => {
log::trace!("Error decoding deflate encoding: {}", err); trace!("Error decoding deflate encoding: {}", err);
Err(err) Err(err)
} }
}, },
@ -383,7 +384,7 @@ impl ContentEncoder {
ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) { ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => { Err(err) => {
log::trace!("Error decoding ztsd encoding: {}", err); trace!("Error decoding ztsd encoding: {}", err);
Err(err) Err(err)
} }
}, },

View File

@ -1,6 +1,7 @@
use std::{io, task::Poll}; use std::{io, task::Poll};
use bytes::{Buf as _, Bytes, BytesMut}; use bytes::{Buf as _, Bytes, BytesMut};
use tracing::{debug, trace};
macro_rules! byte ( macro_rules! byte (
($rdr:ident) => ({ ($rdr:ident) => ({
@ -76,7 +77,7 @@ impl ChunkedState {
Poll::Ready(Ok(ChunkedState::Size)) Poll::Ready(Ok(ChunkedState::Size))
} }
None => { None => {
log::debug!("chunk size would overflow u64"); debug!("chunk size would overflow u64");
Poll::Ready(Err(io::Error::new( Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidInput, io::ErrorKind::InvalidInput,
"Invalid chunk size line: Size is too big", "Invalid chunk size line: Size is too big",
@ -124,7 +125,7 @@ impl ChunkedState {
rem: &mut u64, rem: &mut u64,
buf: &mut Option<Bytes>, buf: &mut Option<Bytes>,
) -> Poll<Result<ChunkedState, io::Error>> { ) -> Poll<Result<ChunkedState, io::Error>> {
log::trace!("Chunked read, remaining={:?}", rem); trace!("Chunked read, remaining={:?}", rem);
let len = rdr.len() as u64; let len = rdr.len() as u64;
if len == 0 { if len == 0 {

View File

@ -6,7 +6,7 @@ use http::{
header::{self, HeaderName, HeaderValue}, header::{self, HeaderName, HeaderValue},
Method, StatusCode, Uri, Version, Method, StatusCode, Uri, Version,
}; };
use log::{debug, error, trace}; use tracing::{debug, error, trace};
use super::chunked::ChunkedState; use super::chunked::ChunkedState;
use crate::{error::ParseError, header::HeaderMap, ConnectionType, Request, ResponseHead}; 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 mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY;
let (len, ver, status, h_len) = { 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<httparse::Header<'_>>; MAX_HEADERS]>::uninit()
.assume_init()
};
let mut res = httparse::Response::new(&mut parsed); let mut res = httparse::Response::new(&mut []);
match res.parse(src)? {
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) => { httparse::Status::Complete(len) => {
let version = if res.version.unwrap() == 1 { let version = if res.version.unwrap() == 1 {
Version::HTTP_11 Version::HTTP_11
} else { } else {
Version::HTTP_10 Version::HTTP_10
}; };
let status = StatusCode::from_u16(res.code.unwrap()) let status = StatusCode::from_u16(res.code.unwrap())
.map_err(|_| ParseError::Status)?; .map_err(|_| ParseError::Status)?;
HeaderIndex::record(src, res.headers, &mut headers); HeaderIndex::record(src, res.headers, &mut headers);
(len, version, status, res.headers.len()) (len, version, status, res.headers.len())
} }
httparse::Status::Partial => { httparse::Status::Partial => {
return if src.len() >= MAX_BUFFER_SIZE { return if src.len() >= MAX_BUFFER_SIZE {
error!("MAX_BUFFER_SIZE unprocessed data reached, closing"); 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] = pub(crate) const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] =
[EMPTY_HEADER_INDEX; 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 { impl HeaderIndex {
pub(crate) fn record( pub(crate) fn record(
bytes: &[u8], bytes: &[u8],

View File

@ -15,6 +15,7 @@ use bitflags::bitflags;
use bytes::{Buf, BytesMut}; use bytes::{Buf, BytesMut};
use futures_core::ready; use futures_core::ready;
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
use tracing::{debug, error, trace};
use crate::{ use crate::{
body::{BodySize, BoxBody, MessageBody}, body::{BodySize, BoxBody, MessageBody},
@ -336,7 +337,7 @@ where
while written < len { while written < len {
match io.as_mut().poll_write(cx, &write_buf[written..])? { match io.as_mut().poll_write(cx, &write_buf[written..])? {
Poll::Ready(0) => { Poll::Ready(0) => {
log::error!("write zero; closing"); error!("write zero; closing");
return Poll::Ready(Err(io::Error::new(io::ErrorKind::WriteZero, ""))); return Poll::Ready(Err(io::Error::new(io::ErrorKind::WriteZero, "")));
} }
@ -375,8 +376,6 @@ where
DispatchError::Io(err) DispatchError::Io(err)
})?; })?;
this.flags.set(Flags::KEEP_ALIVE, this.codec.keep_alive());
Ok(size) Ok(size)
} }
@ -459,7 +458,12 @@ where
} }
// all messages are dealt with // 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 } => { StateProj::ServiceCall { fut } => {
@ -565,7 +569,7 @@ where
} }
StateProj::ExpectCall { fut } => { StateProj::ExpectCall { fut } => {
log::trace!(" calling expect service"); trace!(" calling expect service");
match fut.poll(cx) { match fut.poll(cx) {
// expect resolved. write continue to buffer and set InnerDispatcher state // expect resolved. write continue to buffer and set InnerDispatcher state
@ -694,12 +698,12 @@ where
let mut this = self.as_mut().project(); let mut this = self.as_mut().project();
if can_not_read { if can_not_read {
log::debug!("cannot read request payload"); debug!("cannot read request payload");
if let Some(sender) = &this.payload { if let Some(sender) = &this.payload {
// ...maybe handler does not want to read any more payload... // ...maybe handler does not want to read any more payload...
if let PayloadStatus::Dropped = sender.need_read(cx) { 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 // ...in which case poll request payload a few times
loop { loop {
match this.codec.decode(this.read_buf)? { match this.codec.decode(this.read_buf)? {
@ -713,7 +717,7 @@ where
// connection is in clean state for next request // connection is in clean state for next request
Message::Chunk(None) => { Message::Chunk(None) => {
log::debug!("connection successfully cleaned"); debug!("connection successfully cleaned");
// reset dispatcher state // reset dispatcher state
let _ = this.payload.take(); let _ = this.payload.take();
@ -734,7 +738,7 @@ where
// not enough info to decide if connection is going to be clean or not // not enough info to decide if connection is going to be clean or not
None => { None => {
log::error!( error!(
"handler did not read whole payload and dispatcher could not \ "handler did not read whole payload and dispatcher could not \
drain read buf; return 500 and close connection" drain read buf; return 500 and close connection"
); );
@ -757,6 +761,7 @@ where
let mut updated = false; let mut updated = false;
// decode from read buf as many full requests as possible
loop { loop {
match this.codec.decode(this.read_buf) { match this.codec.decode(this.read_buf) {
Ok(Some(msg)) => { Ok(Some(msg)) => {
@ -809,7 +814,7 @@ where
if let Some(ref mut payload) = this.payload { if let Some(ref mut payload) = this.payload {
payload.feed_data(chunk); payload.feed_data(chunk);
} else { } else {
log::error!("Internal server error: unexpected payload chunk"); error!("Internal server error: unexpected payload chunk");
this.flags.insert(Flags::READ_DISCONNECT); this.flags.insert(Flags::READ_DISCONNECT);
this.messages.push_back(DispatcherMessage::Error( this.messages.push_back(DispatcherMessage::Error(
Response::internal_server_error().drop_body(), Response::internal_server_error().drop_body(),
@ -823,7 +828,7 @@ where
if let Some(mut payload) = this.payload.take() { if let Some(mut payload) = this.payload.take() {
payload.feed_eof(); payload.feed_eof();
} else { } else {
log::error!("Internal server error: unexpected eof"); error!("Internal server error: unexpected eof");
this.flags.insert(Flags::READ_DISCONNECT); this.flags.insert(Flags::READ_DISCONNECT);
this.messages.push_back(DispatcherMessage::Error( this.messages.push_back(DispatcherMessage::Error(
Response::internal_server_error().drop_body(), Response::internal_server_error().drop_body(),
@ -840,7 +845,7 @@ where
Ok(None) => break, Ok(None) => break,
Err(ParseError::Io(err)) => { Err(ParseError::Io(err)) => {
log::trace!("I/O error: {}", &err); trace!("I/O error: {}", &err);
self.as_mut().client_disconnected(); self.as_mut().client_disconnected();
this = self.as_mut().project(); this = self.as_mut().project();
*this.error = Some(DispatchError::Io(err)); *this.error = Some(DispatchError::Io(err));
@ -848,7 +853,7 @@ where
} }
Err(ParseError::TooLarge) => { 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() { if let Some(mut payload) = this.payload.take() {
payload.set_error(PayloadError::Overflow); payload.set_error(PayloadError::Overflow);
@ -868,7 +873,7 @@ where
} }
Err(err) => { Err(err) => {
log::trace!("parse error {}", &err); trace!("parse error {}", &err);
if let Some(mut payload) = this.payload.take() { if let Some(mut payload) = this.payload.take() {
payload.set_error(PayloadError::EncodingCorrupted); payload.set_error(PayloadError::EncodingCorrupted);
@ -899,7 +904,7 @@ where
if timer.as_mut().poll(cx).is_ready() { if timer.as_mut().poll(cx).is_ready() {
// timeout on first request (slow request) return 408 // timeout on first request (slow request) return 408
log::trace!( trace!(
"timed out on slow request; \ "timed out on slow request; \
replying with 408 and closing connection" replying with 408 and closing connection"
); );
@ -945,7 +950,7 @@ where
// keep-alive timer has timed out // keep-alive timer has timed out
if timer.as_mut().poll(cx).is_ready() { if timer.as_mut().poll(cx).is_ready() {
// no tasks at hand // no tasks at hand
log::trace!("timer timed out; closing connection"); trace!("timer timed out; closing connection");
this.flags.insert(Flags::SHUTDOWN); this.flags.insert(Flags::SHUTDOWN);
if let Some(deadline) = this.config.client_disconnect_deadline() { if let Some(deadline) = this.config.client_disconnect_deadline() {
@ -975,7 +980,7 @@ where
// timed-out during shutdown; drop connection // timed-out during shutdown; drop connection
if timer.as_mut().poll(cx).is_ready() { if timer.as_mut().poll(cx).is_ready() {
log::trace!("timed-out during shutdown"); trace!("timed-out during shutdown");
return Err(DispatchError::DisconnectTimeout); return Err(DispatchError::DisconnectTimeout);
} }
} }
@ -1134,12 +1139,12 @@ where
match this.inner.project() { match this.inner.project() {
DispatcherStateProj::Upgrade { fut: upgrade } => upgrade.poll(cx).map_err(|err| { DispatcherStateProj::Upgrade { fut: upgrade } => upgrade.poll(cx).map_err(|err| {
log::error!("Upgrade handler error: {}", err); error!("Upgrade handler error: {}", err);
DispatchError::Upgrade DispatchError::Upgrade
}), }),
DispatcherStateProj::Normal { mut inner } => { DispatcherStateProj::Normal { mut inner } => {
log::trace!("start flags: {:?}", &inner.flags); trace!("start flags: {:?}", &inner.flags);
trace_timer_states( trace_timer_states(
"start", "start",
@ -1246,7 +1251,7 @@ where
// client is gone // client is gone
if inner.flags.contains(Flags::WRITE_DISCONNECT) { if inner.flags.contains(Flags::WRITE_DISCONNECT) {
log::trace!("client is gone; disconnecting"); trace!("client is gone; disconnecting");
return Poll::Ready(Ok(())); return Poll::Ready(Ok(()));
} }
@ -1255,14 +1260,14 @@ where
// read half is closed; we do not process any responses // read half is closed; we do not process any responses
if inner_p.flags.contains(Flags::READ_DISCONNECT) && state_is_none { 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); inner_p.flags.insert(Flags::SHUTDOWN);
} }
// keep-alive and stream errors // keep-alive and stream errors
if state_is_none && inner_p.write_buf.is_empty() { if state_is_none && inner_p.write_buf.is_empty() {
if let Some(err) = inner_p.error.take() { if let Some(err) = inner_p.error.take() {
log::error!("stream error: {}", &err); error!("stream error: {}", &err);
return Poll::Ready(Err(err)); return Poll::Ready(Err(err));
} }
@ -1291,7 +1296,7 @@ where
Poll::Pending Poll::Pending
}; };
log::trace!("end flags: {:?}", &inner.flags); trace!("end flags: {:?}", &inner.flags);
poll poll
} }
@ -1306,17 +1311,17 @@ fn trace_timer_states(
ka_timer: &TimerState, ka_timer: &TimerState,
shutdown_timer: &TimerState, shutdown_timer: &TimerState,
) { ) {
log::trace!("{} timers:", label); trace!("{} timers:", label);
if head_timer.is_enabled() { if head_timer.is_enabled() {
log::trace!(" head {}", &head_timer); trace!(" head {}", &head_timer);
} }
if ka_timer.is_enabled() { if ka_timer.is_enabled() {
log::trace!(" keep-alive {}", &ka_timer); trace!(" keep-alive {}", &ka_timer);
} }
if shutdown_timer.is_enabled() { if shutdown_timer.is_enabled() {
log::trace!(" shutdown {}", &shutdown_timer); trace!(" shutdown {}", &shutdown_timer);
} }
} }

View File

@ -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() { if let Some(c @ b'a'..=b'z') = iter.next() {
buffer[index] = c & 0b1101_1111; buffer[index] = c & 0b1101_1111;
} }
index += 1;
} }
index += 1; index += 1;
@ -528,7 +529,7 @@ mod tests {
use std::rc::Rc; use std::rc::Rc;
use bytes::Bytes; use bytes::Bytes;
use http::header::AUTHORIZATION; use http::header::{AUTHORIZATION, UPGRADE_INSECURE_REQUESTS};
use super::*; use super::*;
use crate::{ use crate::{
@ -559,6 +560,9 @@ mod tests {
head.headers head.headers
.insert(CONTENT_TYPE, HeaderValue::from_static("plain/text")); .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 mut head = RequestHeadType::Owned(head);
let _ = head.encode_headers( let _ = head.encode_headers(
@ -574,6 +578,7 @@ mod tests {
assert!(data.contains("Connection: close\r\n")); assert!(data.contains("Connection: close\r\n"));
assert!(data.contains("Content-Type: plain/text\r\n")); assert!(data.contains("Content-Type: plain/text\r\n"));
assert!(data.contains("Date: date\r\n")); assert!(data.contains("Date: date\r\n"));
assert!(data.contains("Upgrade-Insecure-Requests: 1\r\n"));
let _ = head.encode_headers( let _ = head.encode_headers(
&mut bytes, &mut bytes,

View File

@ -263,7 +263,10 @@ mod tests {
assert_not_impl_any!(Payload: Send, Sync, UnwindSafe, RefUnwindSafe); assert_not_impl_any!(Payload: Send, Sync, UnwindSafe, RefUnwindSafe);
assert_impl_all!(Inner: Unpin, Send, Sync); assert_impl_all!(Inner: Unpin, Send, Sync);
#[rustversion::before(1.60)]
assert_not_impl_any!(Inner: UnwindSafe, RefUnwindSafe); assert_not_impl_any!(Inner: UnwindSafe, RefUnwindSafe);
#[rustversion::since(1.60)]
assert_impl_all!(Inner: UnwindSafe, RefUnwindSafe);
#[actix_rt::test] #[actix_rt::test]
async fn test_unread_data() { async fn test_unread_data() {

View File

@ -13,6 +13,7 @@ use actix_service::{
}; };
use actix_utils::future::ready; use actix_utils::future::ready;
use futures_core::future::LocalBoxFuture; use futures_core::future::LocalBoxFuture;
use tracing::error;
use crate::{ use crate::{
body::{BoxBody, MessageBody}, body::{BoxBody, MessageBody},
@ -305,13 +306,13 @@ where
Box::pin(async move { Box::pin(async move {
let expect = expect let expect = expect
.await .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 { let upgrade = match upgrade {
Some(upgrade) => { Some(upgrade) => {
let upgrade = upgrade let upgrade = upgrade
.await .await
.map_err(|e| log::error!("Init http upgrade service error: {:?}", e))?; .map_err(|e| error!("Init http upgrade service error: {:?}", e))?;
Some(upgrade) Some(upgrade)
} }
None => None, None => None,
@ -319,7 +320,7 @@ where
let service = service let service = service
.await .await
.map_err(|e| log::error!("Init http service error: {:?}", e))?; .map_err(|e| error!("Init http service error: {:?}", e))?;
Ok(H1ServiceHandler::new( Ok(H1ServiceHandler::new(
cfg, cfg,
@ -357,7 +358,7 @@ where
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self._poll_ready(cx).map_err(|err| { 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) DispatchError::Service(err)
}) })
} }

View File

@ -1,6 +1,7 @@
use std::{fmt, future::Future, pin::Pin, task::Context}; use std::{fmt, future::Future, pin::Pin, task::Context};
use actix_rt::time::{Instant, Sleep}; use actix_rt::time::{Instant, Sleep};
use tracing::trace;
#[derive(Debug)] #[derive(Debug)]
pub(super) enum TimerState { pub(super) enum TimerState {
@ -24,7 +25,7 @@ impl TimerState {
pub(super) fn set(&mut self, timer: Sleep, line: u32) { pub(super) fn set(&mut self, timer: Sleep, line: u32) {
if matches!(self, Self::Disabled) { if matches!(self, Self::Disabled) {
log::trace!("setting disabled timer from line {}", line); trace!("setting disabled timer from line {}", line);
} }
*self = Self::Active { *self = Self::Active {
@ -39,11 +40,11 @@ impl TimerState {
pub(super) fn clear(&mut self, line: u32) { pub(super) fn clear(&mut self, line: u32) {
if matches!(self, Self::Disabled) { 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) { 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; *self = Self::Inactive;

View File

@ -19,8 +19,8 @@ use h2::{
server::{Connection, SendResponse}, server::{Connection, SendResponse},
Ping, PingPong, Ping, PingPong,
}; };
use log::{error, trace};
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
use tracing::{error, trace, warn};
use crate::{ use crate::{
body::{BodySize, BoxBody, MessageBody}, body::{BodySize, BoxBody, MessageBody},
@ -143,7 +143,7 @@ where
DispatchError::SendResponse(err) => { DispatchError::SendResponse(err) => {
trace!("Error sending HTTP/2 response: {:?}", err) trace!("Error sending HTTP/2 response: {:?}", err)
} }
DispatchError::SendData(err) => log::warn!("{:?}", err), DispatchError::SendData(err) => warn!("{:?}", err),
DispatchError::ResponseBody(err) => { DispatchError::ResponseBody(err) => {
error!("Response payload stream error: {:?}", err) error!("Response payload stream error: {:?}", err)
} }

View File

@ -14,7 +14,7 @@ use actix_service::{
}; };
use actix_utils::future::ready; use actix_utils::future::ready;
use futures_core::{future::LocalBoxFuture, ready}; use futures_core::{future::LocalBoxFuture, ready};
use log::error; use tracing::{error, trace};
use crate::{ use crate::{
body::{BoxBody, MessageBody}, body::{BoxBody, MessageBody},
@ -355,7 +355,7 @@ where
} }
Err(err) => { Err(err) => {
log::trace!("H2 handshake error: {}", err); trace!("H2 handshake error: {}", err);
Poll::Ready(Err(err)) Poll::Ready(Err(err))
} }
}, },

View File

@ -13,7 +13,8 @@ use crate::error::PayloadError;
/// A boxed payload stream. /// A boxed payload stream.
pub type BoxedPayloadStream = Pin<Box<dyn Stream<Item = Result<Bytes, PayloadError>>>>; pub type BoxedPayloadStream = Pin<Box<dyn Stream<Item = Result<Bytes, PayloadError>>>>;
#[deprecated(since = "4.0.0", note = "Renamed to `BoxedPayloadStream`.")] #[doc(hidden)]
#[deprecated(since = "3.0.0", note = "Renamed to `BoxedPayloadStream`.")]
pub type PayloadStream = BoxedPayloadStream; pub type PayloadStream = BoxedPayloadStream;
#[cfg(not(feature = "http2"))] #[cfg(not(feature = "http2"))]

View File

@ -144,7 +144,7 @@ impl ResponseBuilder {
self self
} }
/// Set connection type to Upgrade /// Set connection type to `Upgrade`.
#[inline] #[inline]
pub fn upgrade<V>(&mut self, value: V) -> &mut Self pub fn upgrade<V>(&mut self, value: V) -> &mut Self
where where
@ -161,7 +161,7 @@ impl ResponseBuilder {
self self
} }
/// Force close connection, even if it is marked as keep-alive /// Force-close connection, even if it is marked as keep-alive.
#[inline] #[inline]
pub fn force_close(&mut self) -> &mut Self { pub fn force_close(&mut self) -> &mut Self {
if let Some(parts) = self.inner() { if let Some(parts) = self.inner() {

View File

@ -15,6 +15,7 @@ use actix_service::{
}; };
use futures_core::{future::LocalBoxFuture, ready}; use futures_core::{future::LocalBoxFuture, ready};
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
use tracing::error;
use crate::{ use crate::{
body::{BoxBody, MessageBody}, body::{BoxBody, MessageBody},
@ -369,13 +370,13 @@ where
Box::pin(async move { Box::pin(async move {
let expect = expect let expect = expect
.await .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 { let upgrade = match upgrade {
Some(upgrade) => { Some(upgrade) => {
let upgrade = upgrade let upgrade = upgrade
.await .await
.map_err(|e| log::error!("Init http upgrade service error: {:?}", e))?; .map_err(|e| error!("Init http upgrade service error: {:?}", e))?;
Some(upgrade) Some(upgrade)
} }
None => None, None => None,
@ -383,7 +384,7 @@ where
let service = service let service = service
.await .await
.map_err(|e| log::error!("Init http service error: {:?}", e))?; .map_err(|e| error!("Init http service error: {:?}", e))?;
Ok(HttpServiceHandler::new( Ok(HttpServiceHandler::new(
cfg, cfg,
@ -490,7 +491,7 @@ where
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self._poll_ready(cx).map_err(|err| { self._poll_ready(cx).map_err(|err| {
log::error!("HTTP service readiness error: {:?}", err); error!("HTTP service readiness error: {:?}", err);
DispatchError::Service(err) DispatchError::Service(err)
}) })
} }
@ -666,7 +667,7 @@ where
self.poll(cx) self.poll(cx)
} }
Err(err) => { Err(err) => {
log::trace!("H2 handshake error: {}", err); tracing::trace!("H2 handshake error: {}", err);
Poll::Ready(Err(err)) Poll::Ready(Err(err))
} }
} }

View File

@ -2,6 +2,7 @@ use actix_codec::{Decoder, Encoder};
use bitflags::bitflags; use bitflags::bitflags;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use bytestring::ByteString; use bytestring::ByteString;
use tracing::error;
use super::{ use super::{
frame::Parser, frame::Parser,
@ -253,7 +254,7 @@ impl Decoder for Codec {
} }
} }
_ => { _ => {
log::error!("Unfinished fragment {:?}", opcode); error!("Unfinished fragment {:?}", opcode);
Err(ProtocolError::ContinuationFragment(opcode)) Err(ProtocolError::ContinuationFragment(opcode))
} }
}; };

View File

@ -73,8 +73,8 @@ mod inner {
use actix_service::{IntoService, Service}; use actix_service::{IntoService, Service};
use futures_core::stream::Stream; use futures_core::stream::Stream;
use local_channel::mpsc; use local_channel::mpsc;
use log::debug;
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
use tracing::debug;
use actix_codec::{AsyncRead, AsyncWrite, Decoder, Encoder, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Decoder, Encoder, Framed};

View File

@ -1,7 +1,7 @@
use std::convert::TryFrom; use std::convert::TryFrom;
use bytes::{Buf, BufMut, BytesMut}; use bytes::{Buf, BufMut, BytesMut};
use log::debug; use tracing::debug;
use super::{ use super::{
mask::apply_mask, mask::apply_mask,

View File

@ -3,6 +3,8 @@ use std::{
fmt, fmt,
}; };
use tracing::error;
/// Operation codes defined in [RFC 6455 §11.8]. /// Operation codes defined in [RFC 6455 §11.8].
/// ///
/// [RFC 6455]: https://datatracker.ietf.org/doc/html/rfc6455#section-11.8 /// [RFC 6455]: https://datatracker.ietf.org/doc/html/rfc6455#section-11.8
@ -58,7 +60,7 @@ impl From<OpCode> for u8 {
Ping => 9, Ping => 9,
Pong => 10, Pong => 10,
Bad => { 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 8 // if this somehow happens, a close frame will help us tear down quickly
} }
} }

View File

@ -212,6 +212,7 @@ async fn h2_content_length() {
let value = HeaderValue::from_static("0"); let value = HeaderValue::from_static("0");
{ {
#[allow(clippy::single_element_loop)]
for &i in &[0] { for &i in &[0] {
let req = srv let req = srv
.request(Method::HEAD, srv.surl(&format!("/{}", i))) .request(Method::HEAD, srv.surl(&format!("/{}", i)))
@ -226,6 +227,7 @@ async fn h2_content_length() {
// assert_eq!(response.headers().get(&header), None); // assert_eq!(response.headers().get(&header), None);
} }
#[allow(clippy::single_element_loop)]
for &i in &[1] { for &i in &[1] {
let req = srv let req = srv
.request(Method::GET, srv.surl(&format!("/{}", i))) .request(Method::GET, srv.surl(&format!("/{}", i)))

View File

@ -14,7 +14,7 @@ name = "actix_multipart"
path = "src/lib.rs" path = "src/lib.rs"
[dependencies] [dependencies]
actix-utils = "3.0.0" actix-utils = "3"
actix-web = { version = "4.0.0", default-features = false } actix-web = { version = "4.0.0", default-features = false }
bytes = "1" bytes = "1"

View File

@ -23,9 +23,9 @@ default = ["http"]
bytestring = ">=0.1.5, <2" bytestring = ">=0.1.5, <2"
firestorm = "0.5" firestorm = "0.5"
http = { version = "0.2.3", optional = true } http = { version = "0.2.3", optional = true }
log = "0.4"
regex = "1.5" regex = "1.5"
serde = "1" serde = "1"
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
[dev-dependencies] [dev-dependencies]
criterion = { version = "0.3", features = ["html_reports"] } criterion = { version = "0.3", features = ["html_reports"] }

View File

@ -7,6 +7,7 @@ use std::{
use firestorm::{profile_fn, profile_method, profile_section}; use firestorm::{profile_fn, profile_method, profile_section};
use regex::{escape, Regex, RegexSet}; use regex::{escape, Regex, RegexSet};
use tracing::error;
use crate::{path::PathItem, IntoPatterns, Patterns, Resource, ResourcePath}; use crate::{path::PathItem, IntoPatterns, Patterns, Resource, ResourcePath};
@ -714,10 +715,7 @@ impl ResourceDef {
if let Some(m) = captures.name(name) { if let Some(m) = captures.name(name) {
segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16); segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16);
} else { } else {
log::error!( error!("Dynamic path match but not all segments found: {}", name);
"Dynamic path match but not all segments found: {}",
name
);
return false; return false;
} }
} }
@ -744,7 +742,7 @@ impl ResourceDef {
if let Some(m) = captures.name(name) { if let Some(m) = captures.name(name) {
segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16); segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16);
} else { } else {
log::error!("Dynamic path match but not all segments found: {}", name); error!("Dynamic path match but not all segments found: {}", name);
return false; return false;
} }
} }
@ -1038,7 +1036,7 @@ impl ResourceDef {
// tail segments in prefixes have no defined semantics // tail segments in prefixes have no defined semantics
#[cfg(not(test))] #[cfg(not(test))]
log::warn!( tracing::warn!(
"Prefix resources should not have tail segments. \ "Prefix resources should not have tail segments. \
Use `ResourceDef::new` constructor. \ Use `ResourceDef::new` constructor. \
This may become a panic in the future." This may become a panic in the future."
@ -1053,7 +1051,7 @@ impl ResourceDef {
// unnamed tail segment // unnamed tail segment
#[cfg(not(test))] #[cfg(not(test))]
log::warn!( tracing::warn!(
"Tail segments must have names. \ "Tail segments must have names. \
Consider `.../{{tail}}*`. \ Consider `.../{{tail}}*`. \
This may become a panic in the future." This may become a panic in the future."

View File

@ -29,13 +29,13 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"]
[dependencies] [dependencies]
actix-codec = "0.5" actix-codec = "0.5"
actix-http = "3.0.0" actix-http = "3"
actix-http-test = "3.0.0-beta.13" actix-http-test = "3.0.0-beta.13"
actix-rt = "2.1" actix-rt = "2.1"
actix-service = "2.0.0" actix-service = "2"
actix-utils = "3.0.0" actix-utils = "3"
actix-web = { version = "4.0.0", default-features = false, features = ["cookies"] } actix-web = { version = "4", default-features = false, features = ["cookies"] }
awc = { version = "3.0.0-beta.21", 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-core = { version = "0.3.7", default-features = false, features = ["std"] }
futures-util = { version = "0.3.7", default-features = false, features = [] } futures-util = { version = "0.3.7", default-features = false, features = [] }

View File

@ -28,7 +28,7 @@ tokio = { version = "1.13.1", features = ["sync"] }
[dev-dependencies] [dev-dependencies]
actix-rt = "2.2" actix-rt = "2.2"
actix-test = "0.1.0-beta.13" 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" env_logger = "0.9"
futures-util = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false }

View File

@ -20,7 +20,7 @@
- Updated `cookie` to `0.16`. [#2555] - Updated `cookie` to `0.16`. [#2555]
- Updated `language-tags` to `0.3`. - Updated `language-tags` to `0.3`.
- Updated `rand` to `0.8`. - Updated `rand` to `0.8`.
- Updated `rustls` to `0.20.0`. [#2414] - Updated `rustls` to `0.20`. [#2414]
- Updated `tokio` to `1`. - Updated `tokio` to `1`.
### Added ### Added

View File

@ -71,9 +71,9 @@ actix-service = "2"
actix-utils = "3" actix-utils = "3"
actix-tls = { version = "3", default-features = false, optional = true } actix-tls = { version = "3", default-features = false, optional = true }
actix-http = { version = "3.0.0", features = ["http2", "ws"] } actix-http = { version = "3", features = ["http2", "ws"] }
actix-router = "0.5.0" actix-router = "0.5"
actix-web-codegen = { version = "4.0.0", optional = true } actix-web-codegen = { version = "4", optional = true }
ahash = "0.7" ahash = "0.7"
bytes = "1" bytes = "1"
@ -90,7 +90,7 @@ once_cell = "1.5"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
pin-project-lite = "0.2.7" pin-project-lite = "0.2.7"
regex = "1.4" regex = "1.5.5"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
serde_urlencoded = "0.7" serde_urlencoded = "0.7"
@ -100,9 +100,9 @@ time = { version = "0.3", default-features = false, features = ["formatting"] }
url = "2.1" url = "2.1"
[dev-dependencies] [dev-dependencies]
actix-files = "0.6.0" actix-files = "0.6"
actix-test = { version = "0.1.0-beta.13", features = ["openssl", "rustls"] } 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" brotli = "3.3.3"
const-str = "0.3" const-str = "0.3"

View File

@ -56,18 +56,19 @@ Code:
```rust ```rust
use actix_web::{get, web, App, HttpServer, Responder}; use actix_web::{get, web, App, HttpServer, Responder};
#[get("/{id}/{name}/index.html")] #[get("/hello/{name}")]
async fn index(params: web::Path<(u32, String)>) -> impl Responder { async fn greet(name: web::Path<String>) -> impl Responder {
let (id, name) = params.into_inner(); format!("Hello {name}!")
format!("Hello {}! id:{}", name, id)
} }
#[actix_web::main] // or #[tokio::main] #[actix_web::main] // or #[tokio::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new().service(index)) HttpServer::new(|| {
.bind(("127.0.0.1", 8080))? App::new().service(greet)
.run() })
.await .bind(("127.0.0.1", 8080))?
.run()
.await
} }
``` ```

View File

@ -4,18 +4,19 @@
//! ```no_run //! ```no_run
//! use actix_web::{get, web, App, HttpServer, Responder}; //! use actix_web::{get, web, App, HttpServer, Responder};
//! //!
//! #[get("/{id}/{name}/index.html")] //! #[get("/hello/{name}")]
//! async fn index(path: web::Path<(u32, String)>) -> impl Responder { //! async fn greet(name: web::Path<String>) -> impl Responder {
//! let (id, name) = path.into_inner(); //! format!("Hello {}!", name)
//! format!("Hello {}! id:{}", name, id)
//! } //! }
//! //!
//! #[actix_web::main] //! #[actix_web::main] // or #[tokio::main]
//! async fn main() -> std::io::Result<()> { //! async fn main() -> std::io::Result<()> {
//! HttpServer::new(|| App::new().service(index)) //! HttpServer::new(|| {
//! .bind("127.0.0.1:8080")? //! App::new().service(greet)
//! .run() //! })
//! .await //! .bind(("127.0.0.1", 8080))?
//! .run()
//! .await
//! } //! }
//! ``` //! ```
//! //!

View File

@ -367,9 +367,7 @@ where
.local_addr(addr); .local_addr(addr);
let svc = if let Some(handler) = on_connect_fn.clone() { let svc = if let Some(handler) = on_connect_fn.clone() {
svc.on_connect_ext(move |io: &_, ext: _| { svc.on_connect_ext(move |io: &_, ext: _| (handler)(io as &dyn Any, ext))
(&*handler)(io as &dyn Any, ext)
})
} else { } else {
svc svc
}; };
@ -555,7 +553,7 @@ where
if let Some(handler) = on_connect_fn.clone() { if let Some(handler) = on_connect_fn.clone() {
svc = svc 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() let fac = factory()

View File

@ -78,18 +78,18 @@ pub struct ServiceRequest {
} }
impl ServiceRequest { impl ServiceRequest {
/// Construct service request /// Construct `ServiceRequest` from parts.
pub(crate) fn new(req: HttpRequest, payload: Payload) -> Self { pub(crate) fn new(req: HttpRequest, payload: Payload) -> Self {
Self { req, payload } Self { req, payload }
} }
/// Deconstruct request into parts /// Deconstruct `ServiceRequest` into inner parts.
#[inline] #[inline]
pub fn into_parts(self) -> (HttpRequest, Payload) { pub fn into_parts(self) -> (HttpRequest, Payload) {
(self.req, self.payload) (self.req, self.payload)
} }
/// Get mutable access to inner `HttpRequest` and `Payload` /// Returns mutable accessors to inner parts.
#[inline] #[inline]
pub fn parts_mut(&mut self) -> (&mut HttpRequest, &mut Payload) { pub fn parts_mut(&mut self) -> (&mut HttpRequest, &mut Payload) {
(&mut self.req, &mut self.payload) (&mut self.req, &mut self.payload)
@ -105,9 +105,7 @@ impl ServiceRequest {
Self { req, payload } Self { req, payload }
} }
/// Construct request from request. /// Construct `ServiceRequest` with no payload from given `HttpRequest`.
///
/// The returned `ServiceRequest` would have no payload.
#[inline] #[inline]
pub fn from_request(req: HttpRequest) -> Self { pub fn from_request(req: HttpRequest) -> Self {
ServiceRequest { ServiceRequest {
@ -116,63 +114,63 @@ impl ServiceRequest {
} }
} }
/// Create service response /// Create `ServiceResponse` from this request and given response.
#[inline] #[inline]
pub fn into_response<B, R: Into<Response<B>>>(self, res: R) -> ServiceResponse<B> { pub fn into_response<B, R: Into<Response<B>>>(self, res: R) -> ServiceResponse<B> {
let res = HttpResponse::from(res.into()); let res = HttpResponse::from(res.into());
ServiceResponse::new(self.req, res) ServiceResponse::new(self.req, res)
} }
/// Create service response for error /// Create `ServiceResponse` from this request and given error.
#[inline] #[inline]
pub fn error_response<E: Into<Error>>(self, err: E) -> ServiceResponse { pub fn error_response<E: Into<Error>>(self, err: E) -> ServiceResponse {
let res = HttpResponse::from_error(err.into()); let res = HttpResponse::from_error(err.into());
ServiceResponse::new(self.req, res) ServiceResponse::new(self.req, res)
} }
/// This method returns reference to the request head /// Returns a reference to the request head.
#[inline] #[inline]
pub fn head(&self) -> &RequestHead { pub fn head(&self) -> &RequestHead {
self.req.head() self.req.head()
} }
/// This method returns reference to the request head /// Returns a mutable reference to the request head.
#[inline] #[inline]
pub fn head_mut(&mut self) -> &mut RequestHead { pub fn head_mut(&mut self) -> &mut RequestHead {
self.req.head_mut() self.req.head_mut()
} }
/// Request's uri. /// Returns the request URI.
#[inline] #[inline]
pub fn uri(&self) -> &Uri { pub fn uri(&self) -> &Uri {
&self.head().uri &self.head().uri
} }
/// Read the Request method. /// Returns the request method.
#[inline] #[inline]
pub fn method(&self) -> &Method { pub fn method(&self) -> &Method {
&self.head().method &self.head().method
} }
/// Read the Request Version. /// Returns the request version.
#[inline] #[inline]
pub fn version(&self) -> Version { pub fn version(&self) -> Version {
self.head().version self.head().version
} }
/// Returns a reference to request headers.
#[inline] #[inline]
/// Returns request's headers.
pub fn headers(&self) -> &HeaderMap { pub fn headers(&self) -> &HeaderMap {
&self.head().headers &self.head().headers
} }
/// Returns a mutable reference to request headers.
#[inline] #[inline]
/// Returns mutable request's headers.
pub fn headers_mut(&mut self) -> &mut HeaderMap { pub fn headers_mut(&mut self) -> &mut HeaderMap {
&mut self.head_mut().headers &mut self.head_mut().headers
} }
/// The target path of this Request. /// Returns request path.
#[inline] #[inline]
pub fn path(&self) -> &str { pub fn path(&self) -> &str {
self.head().uri.path() self.head().uri.path()
@ -184,7 +182,7 @@ impl ServiceRequest {
self.req.query_string() 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 /// 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. /// the Actix Web server, then it would be address of this proxy.
@ -197,24 +195,23 @@ impl ServiceRequest {
self.head().peer_addr self.head().peer_addr
} }
/// Get *ConnectionInfo* for the current request. /// Returns a reference to connection info.
#[inline] #[inline]
pub fn connection_info(&self) -> Ref<'_, ConnectionInfo> { pub fn connection_info(&self) -> Ref<'_, ConnectionInfo> {
self.req.connection_info() self.req.connection_info()
} }
/// Returns a reference to the Path parameters. /// Returns reference to the Path parameters.
/// ///
/// Params is a container for URL parameters. /// Params is a container for URL parameters. A variable segment is specified in the form
/// A variable segment is specified in the form `{identifier}`, /// `{identifier}`, where the identifier can be used later in a request handler to access the
/// where the identifier can be used later in a request handler to /// matched value for that segment.
/// access the matched value for that segment.
#[inline] #[inline]
pub fn match_info(&self) -> &Path<Url> { pub fn match_info(&self) -> &Path<Url> {
self.req.match_info() self.req.match_info()
} }
/// Returns a mutable reference to the Path parameters. /// Returns a mutable reference to the path match information.
#[inline] #[inline]
pub fn match_info_mut(&mut self) -> &mut Path<Url> { pub fn match_info_mut(&mut self) -> &mut Path<Url> {
self.req.match_info_mut() self.req.match_info_mut()
@ -232,13 +229,13 @@ impl ServiceRequest {
self.req.match_pattern() self.req.match_pattern()
} }
/// Get a reference to a `ResourceMap` of current application. /// Returns a reference to the application's resource map.
#[inline] #[inline]
pub fn resource_map(&self) -> &ResourceMap { pub fn resource_map(&self) -> &ResourceMap {
self.req.resource_map() self.req.resource_map()
} }
/// Service configuration /// Returns a reference to the application's configuration.
#[inline] #[inline]
pub fn app_config(&self) -> &AppConfig { pub fn app_config(&self) -> &AppConfig {
self.req.app_config() self.req.app_config()
@ -262,6 +259,7 @@ impl ServiceRequest {
self.req.conn_data() self.req.conn_data()
} }
/// Return request cookies.
#[cfg(feature = "cookies")] #[cfg(feature = "cookies")]
#[inline] #[inline]
pub fn cookies(&self) -> Result<Ref<'_, Vec<Cookie<'static>>>, CookieParseError> { pub fn cookies(&self) -> Result<Ref<'_, Vec<Cookie<'static>>>, CookieParseError> {

View File

@ -3,6 +3,103 @@
## Unreleased - 2021-xx-xx ## 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<dyn std::error::Error>` 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<T, U>` 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
<details>
<summary>3.0.0 Pre-Releases</summary>
## 3.0.0-beta.21 - 2022-02-16 ## 3.0.0-beta.21 - 2022-02-16
- No significant changes since `3.0.0-beta.20`. - No significant changes since `3.0.0-beta.20`.
@ -170,6 +267,7 @@
[#1813]: https://github.com/actix/actix-web/pull/1813 [#1813]: https://github.com/actix/actix-web/pull/1813
</details>
## 2.0.3 - 2020-11-29 ## 2.0.3 - 2020-11-29
### Fixed ### Fixed

View File

@ -1,11 +1,11 @@
[package] [package]
name = "awc" name = "awc"
version = "3.0.0-beta.21" version = "3.0.0"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"fakeshadow <24548779@qq.com>", "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"] keywords = ["actix", "http", "framework", "async", "web"]
categories = [ categories = [
"network-programming", "network-programming",
@ -59,11 +59,11 @@ dangerous-h2c = []
[dependencies] [dependencies]
actix-codec = "0.5" actix-codec = "0.5"
actix-service = "2.0.0" actix-service = "2"
actix-http = { version = "3.0.0", features = ["http2", "ws"] } actix-http = { version = "3", features = ["http2", "ws"] }
actix-rt = { version = "2.1", default-features = false } actix-rt = { version = "2.1", default-features = false }
actix-tls = { version = "3", features = ["connect", "uri"] } actix-tls = { version = "3", features = ["connect", "uri"] }
actix-utils = "3.0.0" actix-utils = "3"
ahash = "0.7" ahash = "0.7"
base64 = "0.13" 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 } trust-dns-resolver = { version = "0.20.0", optional = true }
[dev-dependencies] [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-http-test = { version = "3.0.0-beta.13", features = ["openssl"] }
actix-server = "2" actix-server = "2"
actix-test = { version = "0.1.0-beta.13", features = ["openssl", "rustls"] } actix-test = { version = "0.1.0-beta.13", features = ["openssl", "rustls"] }
actix-tls = { version = "3", features = ["openssl", "rustls"] } actix-tls = { version = "3", features = ["openssl", "rustls"] }
actix-utils = "3.0.0" actix-utils = "3"
actix-web = { version = "4.0.0", features = ["openssl"] } actix-web = { version = "4", features = ["openssl"] }
brotli = "3.3.3" brotli = "3.3.3"
const-str = "0.3" const-str = "0.3"

View File

@ -3,9 +3,9 @@
> Async HTTP and WebSocket client library. > Async HTTP and WebSocket client library.
[![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc) [![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) ![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) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
## Documentation & Resources ## Documentation & Resources

View File

@ -246,7 +246,12 @@ where
/// ///
/// The default limit size is 100. /// The default limit size is 100.
pub fn limit(mut self, limit: usize) -> Self { 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 self
} }

View File

@ -83,12 +83,12 @@ where
false false
}; };
framed.send((head, body.size()).into()).await?;
let mut pin_framed = Pin::new(&mut framed); let mut pin_framed = Pin::new(&mut framed);
// special handle for EXPECT request. // special handle for EXPECT request.
let (do_send, mut res_head) = if is_expect { 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)) let head = poll_fn(|cx| pin_framed.as_mut().poll_next(cx))
.await .await
.ok_or(ConnectError::Disconnected)??; .ok_or(ConnectError::Disconnected)??;
@ -97,13 +97,17 @@ where
// and current head would be used as final response head. // and current head would be used as final response head.
(head.status == StatusCode::CONTINUE, Some(head)) (head.status == StatusCode::CONTINUE, Some(head))
} else { } else {
pin_framed.feed((head, body.size()).into()).await?;
(true, None) (true, None)
}; };
if do_send { if do_send {
// send request body // send request body
match body.size() { 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?, _ => send_body(body, pin_framed.as_mut()).await?,
}; };

View File

@ -30,17 +30,35 @@ pub type BoxConnectorService = Rc<
pub type BoxedSocket = Box<dyn ConnectionIo>; pub type BoxedSocket = Box<dyn ConnectionIo>;
/// Combined HTTP and WebSocket request type received by connection service.
pub enum ConnectRequest { pub enum ConnectRequest {
/// Standard HTTP request.
///
/// Contains the request head, body type, and optional pre-resolved socket address.
Client(RequestHeadType, AnyBody, Option<net::SocketAddr>), Client(RequestHeadType, AnyBody, Option<net::SocketAddr>),
/// Tunnel used by WebSocket connection requests.
///
/// Contains the request head and optional pre-resolved socket address.
Tunnel(RequestHead, Option<net::SocketAddr>), Tunnel(RequestHead, Option<net::SocketAddr>),
} }
/// Combined HTTP response & WebSocket tunnel type returned from connection service.
pub enum ConnectResponse { pub enum ConnectResponse {
/// Standard HTTP response.
Client(ClientResponse), Client(ClientResponse),
/// Tunnel used for WebSocket communication.
///
/// Contains response head and framed HTTP/1.1 codec.
Tunnel(ResponseHead, Framed<BoxedSocket, ClientCodec>), Tunnel(ResponseHead, Framed<BoxedSocket, ClientCodec>),
} }
impl ConnectResponse { impl ConnectResponse {
/// Unwraps type into HTTP response.
///
/// # Panics
/// Panics if enum variant is not `Client`.
pub fn into_client_response(self) -> ClientResponse { pub fn into_client_response(self) -> ClientResponse {
match self { match self {
ConnectResponse::Client(res) => res, 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<BoxedSocket, ClientCodec>) { pub fn into_tunnel_response(self) -> (ResponseHead, Framed<BoxedSocket, ClientCodec>) {
match self { match self {
ConnectResponse::Tunnel(head, framed) => (head, framed), ConnectResponse::Tunnel(head, framed) => (head, framed),
@ -136,30 +158,37 @@ where
ConnectRequestProj::Connection { fut, req } => { ConnectRequestProj::Connection { fut, req } => {
let connection = ready!(fut.poll(cx))?; let connection = ready!(fut.poll(cx))?;
let req = req.take().unwrap(); let req = req.take().unwrap();
match req { match req {
ConnectRequest::Client(head, body, ..) => { ConnectRequest::Client(head, body, ..) => {
// send request // send request
let fut = ConnectRequestFuture::Client { let fut = ConnectRequestFuture::Client {
fut: connection.send_request(head, body), fut: connection.send_request(head, body),
}; };
self.set(fut); self.set(fut);
} }
ConnectRequest::Tunnel(head, ..) => { ConnectRequest::Tunnel(head, ..) => {
// send request // send request
let fut = ConnectRequestFuture::Tunnel { let fut = ConnectRequestFuture::Tunnel {
fut: connection.open_tunnel(RequestHeadType::from(head)), fut: connection.open_tunnel(RequestHeadType::from(head)),
}; };
self.set(fut); self.set(fut);
} }
} }
self.poll(cx) self.poll(cx)
} }
ConnectRequestProj::Client { fut } => { ConnectRequestProj::Client { fut } => {
let (head, payload) = ready!(fut.as_mut().poll(cx))?; let (head, payload) = ready!(fut.as_mut().poll(cx))?;
Poll::Ready(Ok(ConnectResponse::Client(ClientResponse::new( Poll::Ready(Ok(ConnectResponse::Client(ClientResponse::new(
head, payload, head, payload,
)))) ))))
} }
ConnectRequestProj::Tunnel { fut } => { ConnectRequestProj::Tunnel { fut } => {
let (head, framed) = ready!(fut.as_mut().poll(cx))?; let (head, framed) = ready!(fut.as_mut().poll(cx))?;
let framed = framed.into_map_io(|io| Box::new(io) as _); let framed = framed.into_map_io(|io| Box::new(io) as _);

View File

@ -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 //! ```no_run
//! # #[actix_rt::main] //! # #[actix_rt::main]
//! # async fn main() -> Result<(), awc::error::SendRequestError> { //! # async fn main() -> Result<(), awc::error::SendRequestError> {
//! // create client
//! let mut client = awc::Client::default(); //! 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(()) //! # Ok(())
//! # } //! # }
//! ``` //! ```
//! //!
//! # Making POST requests //! # `POST` Requests
//! ## Raw body contents //! ## Raw Body
//! ```no_run //! ```no_run
//! # #[actix_rt::main] //! # #[actix_rt::main]
//! # async fn main() -> Result<(), awc::error::SendRequestError> { //! # 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(&params)
//! .await?;
//! # Ok(())
//! # }
//! ```
//!
//! ## JSON //! ## JSON
//! ```no_run //! ```no_run
//! # #[actix_rt::main] //! # #[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(&params)
//! .await?;
//! # Ok(())
//! # }
//! ```
//!
//! # Response Compression //! # Response Compression
//! All [official][iana-encodings] and common content encoding codecs are supported, optionally. //! 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 //! [iana-encodings]: https://www.iana.org/assignments/http-parameters/http-parameters.xhtml#content-coding
//! //!
//! # WebSocket support //! # WebSockets
//! ```no_run //! ```no_run
//! # #[actix_rt::main] //! # #[actix_rt::main]
//! # async fn main() -> Result<(), Box<dyn std::error::Error>> { //! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! use futures_util::{sink::SinkExt, stream::StreamExt}; //! use futures_util::{sink::SinkExt as _, stream::StreamExt as _};
//!
//! let (_resp, mut connection) = awc::Client::new() //! let (_resp, mut connection) = awc::Client::new()
//! .ws("ws://echo.websocket.org") //! .ws("ws://echo.websocket.org")
//! .connect() //! .connect()
@ -89,8 +93,9 @@
//! connection //! connection
//! .send(awc::ws::Message::Text("Echo".into())) //! .send(awc::ws::Message::Text("Echo".into()))
//! .await?; //! .await?;
//!
//! let response = connection.next().await.unwrap()?; //! 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(()) //! # Ok(())
//! # } //! # }
//! ``` //! ```

View File

@ -161,7 +161,8 @@ where
| StatusCode::SEE_OTHER | StatusCode::SEE_OTHER
| StatusCode::TEMPORARY_REDIRECT | StatusCode::TEMPORARY_REDIRECT
| StatusCode::PERMANENT_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 let reuse_body = res.head().status == StatusCode::TEMPORARY_REDIRECT
|| res.head().status == StatusCode::PERMANENT_REDIRECT; || res.head().status == StatusCode::PERMANENT_REDIRECT;
@ -245,26 +246,32 @@ where
} }
fn build_next_uri(res: &ClientResponse, prev_uri: &Uri) -> Result<Uri, SendRequestError> { fn build_next_uri(res: &ClientResponse, prev_uri: &Uri) -> Result<Uri, SendRequestError> {
let uri = res // responses without this header are not processed
.headers() let location = res.headers().get(header::LOCATION).unwrap();
.get(header::LOCATION)
.map(|value| { // try to parse the location and resolve to a full URI but fall back to default if it fails
// try to parse the location to a full uri let uri = Uri::try_from(location.as_bytes()).unwrap_or_else(|_| Uri::default());
let uri = Uri::try_from(value.as_bytes())
.map_err(|e| SendRequestError::Url(InvalidUrl::HttpError(e.into())))?; let uri = if uri.scheme().is_none() || uri.authority().is_none() {
if uri.scheme().is_none() || uri.authority().is_none() { let builder = Uri::builder()
let uri = Uri::builder() .scheme(prev_uri.scheme().cloned().unwrap())
.scheme(prev_uri.scheme().cloned().unwrap()) .authority(prev_uri.authority().cloned().unwrap());
.authority(prev_uri.authority().cloned().unwrap())
.path_and_query(value.as_bytes()) // when scheme or authority is missing treat the location value as path and query
.build()?; // recover error where location does not have leading slash
Ok::<_, SendRequestError>(uri) let path = if location.as_bytes().starts_with(b"/") {
} else { location.as_bytes().to_owned()
Ok(uri) } else {
} [b"/", location.as_bytes()].concat()
}) };
// TODO: this error type is wrong.
.ok_or(SendRequestError::Url(InvalidUrl::MissingScheme))??; builder
.path_and_query(path)
.build()
.map_err(|err| SendRequestError::Url(InvalidUrl::HttpError(err)))?
} else {
uri
};
Ok(uri) Ok(uri)
} }
@ -287,10 +294,13 @@ mod tests {
use actix_web::{web, App, Error, HttpRequest, HttpResponse}; use actix_web::{web, App, Error, HttpRequest, HttpResponse};
use super::*; use super::*;
use crate::{http::header::HeaderValue, ClientBuilder}; use crate::{
http::{header::HeaderValue, StatusCode},
ClientBuilder,
};
#[actix_rt::test] #[actix_rt::test]
async fn test_basic_redirect() { async fn basic_redirect() {
let client = ClientBuilder::new() let client = ClientBuilder::new()
.disable_redirects() .disable_redirects()
.wrap(Redirect::new().max_redirect_times(10)) .wrap(Redirect::new().max_redirect_times(10))
@ -315,6 +325,44 @@ mod tests {
assert_eq!(res.status().as_u16(), 400); 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] #[actix_rt::test]
async fn test_redirect_limit() { async fn test_redirect_limit() {
let client = ClientBuilder::new() let client = ClientBuilder::new()
@ -328,14 +376,14 @@ mod tests {
.service(web::resource("/").route(web::to(|| async { .service(web::resource("/").route(web::to(|| async {
Ok::<_, Error>( Ok::<_, Error>(
HttpResponse::Found() HttpResponse::Found()
.append_header(("location", "/test")) .insert_header(("location", "/test"))
.finish(), .finish(),
) )
}))) })))
.service(web::resource("/test").route(web::to(|| async { .service(web::resource("/test").route(web::to(|| async {
Ok::<_, Error>( Ok::<_, Error>(
HttpResponse::Found() HttpResponse::Found()
.append_header(("location", "/test2")) .insert_header(("location", "/test2"))
.finish(), .finish(),
) )
}))) })))
@ -345,8 +393,15 @@ mod tests {
}); });
let res = client.get(srv.url("/")).send().await.unwrap(); let res = client.get(srv.url("/")).send().await.unwrap();
assert_eq!(res.status(), StatusCode::FOUND);
assert_eq!(res.status().as_u16(), 302); assert_eq!(
res.headers()
.get(header::LOCATION)
.unwrap()
.to_str()
.unwrap(),
"/test2"
);
} }
#[actix_rt::test] #[actix_rt::test]

View File

@ -505,7 +505,7 @@ impl fmt::Debug for ClientRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!( writeln!(
f, f,
"\nClientRequest {:?} {}:{}", "\nClientRequest {:?} {} {}",
self.head.version, self.head.method, self.head.uri self.head.version, self.head.method, self.head.uri
)?; )?;
writeln!(f, " headers:")?; writeln!(f, " headers:")?;

View File

@ -160,7 +160,7 @@ where
/// ///
/// # Errors /// # Errors
/// `Future` implementation returns error if: /// `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 /// # Examples
/// ```no_run /// ```no_run