Merge branch 'master' into multipart-forms

This commit is contained in:
Rob Ede 2023-02-26 01:07:04 +00:00 committed by GitHub
commit 8578dadbb2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
95 changed files with 1243 additions and 423 deletions

View File

@ -1,8 +1,12 @@
# Changes # Changes
## Unreleased - 2022-xx-xx ## Unreleased - 2022-xx-xx
## 0.6.3 - 2023-01-21
- XHTML files now use `Content-Disposition: inline` instead of `attachment`. [#2903] - XHTML files now use `Content-Disposition: inline` instead of `attachment`. [#2903]
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency. - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
- Update `tokio-uring` dependency to `0.4`.
[#2903]: https://github.com/actix/actix-web/pull/2903 [#2903]: https://github.com/actix/actix-web/pull/2903

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-files" name = "actix-files"
version = "0.6.2" version = "0.6.3"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>", "Rob Ede <robjtede@icloud.com>",
@ -29,7 +29,7 @@ actix-web = { version = "4", default-features = false }
bitflags = "1" bitflags = "1"
bytes = "1" bytes = "1"
derive_more = "0.99.5" derive_more = "0.99.5"
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
http-range = "0.1.4" http-range = "0.1.4"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
@ -40,8 +40,8 @@ v_htmlescape= "0.15"
# experimental-io-uring # experimental-io-uring
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
tokio-uring = { version = "0.3", optional = true, features = ["bytes"] } tokio-uring = { version = "0.4", optional = true, features = ["bytes"] }
actix-server = { version = "2.1", optional = true } # ensure matching tokio-uring versions actix-server = { version = "2.2", optional = true } # ensure matching tokio-uring versions
[dev-dependencies] [dev-dependencies]
actix-rt = "2.7" actix-rt = "2.7"

View File

@ -3,11 +3,11 @@
> Static file serving for Actix Web > Static file serving for Actix Web
[![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files)
[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.2)](https://docs.rs/actix-files/0.6.2) [![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.3)](https://docs.rs/actix-files/0.6.3)
![Version](https://img.shields.io/badge/rustc-1.59+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.59+-ab6000.svg)
![License](https://img.shields.io/crates/l/actix-files.svg) ![License](https://img.shields.io/crates/l/actix-files.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-files/0.6.2/status.svg)](https://deps.rs/crate/actix-files/0.6.2) [![dependency status](https://deps.rs/crate/actix-files/0.6.3/status.svg)](https://deps.rs/crate/actix-files/0.6.3)
[![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) [![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files)
[![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)

View File

@ -142,7 +142,7 @@ impl Files {
self self
} }
/// Set custom directory renderer /// Set custom directory renderer.
pub fn files_listing_renderer<F>(mut self, f: F) -> Self pub fn files_listing_renderer<F>(mut self, f: F) -> Self
where where
for<'r, 's> F: for<'r, 's> F:
@ -152,7 +152,7 @@ impl Files {
self self
} }
/// Specifies mime override callback /// Specifies MIME override callback.
pub fn mime_override<F>(mut self, f: F) -> Self pub fn mime_override<F>(mut self, f: F) -> Self
where where
F: Fn(&mime::Name<'_>) -> DispositionType + 'static, F: Fn(&mime::Name<'_>) -> DispositionType + 'static,
@ -390,3 +390,42 @@ impl ServiceFactory<ServiceRequest> for Files {
} }
} }
} }
#[cfg(test)]
mod tests {
use actix_web::{
http::StatusCode,
test::{self, TestRequest},
App, HttpResponse,
};
use super::*;
#[actix_web::test]
async fn custom_files_listing_renderer() {
let srv = test::init_service(
App::new().service(
Files::new("/", "./tests")
.show_files_listing()
.files_listing_renderer(|dir, req| {
Ok(ServiceResponse::new(
req.clone(),
HttpResponse::Ok().body(dir.path.to_str().unwrap().to_owned()),
))
}),
),
)
.await;
let req = TestRequest::with_uri("/").to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
let body = test::read_body(res).await;
assert!(
body.ends_with(b"actix-files/tests/"),
"body {:?} does not end with `actix-files/tests/`",
body
);
}
}

View File

@ -13,6 +13,7 @@
#![deny(rust_2018_idioms, nonstandard_style)] #![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible, missing_docs, missing_debug_implementations)] #![warn(future_incompatible, missing_docs, missing_debug_implementations)]
#![allow(clippy::uninlined_format_args)]
use actix_service::boxed::{BoxService, BoxServiceFactory}; use actix_service::boxed::{BoxService, BoxServiceFactory};
use actix_web::{ use actix_web::{

View File

@ -30,7 +30,7 @@ impl PathBufWrap {
let mut segment_count = path.matches('/').count() + 1; let mut segment_count = path.matches('/').count() + 1;
// we can decode the whole path here (instead of per-segment decoding) // we can decode the whole path here (instead of per-segment decoding)
// because we will reject `%2F` in paths using `segement_count`. // because we will reject `%2F` in paths using `segment_count`.
let path = percent_encoding::percent_decode_str(path) let path = percent_encoding::percent_decode_str(path)
.decode_utf8() .decode_utf8()
.map_err(|_| UriSegmentError::NotValidUtf8)?; .map_err(|_| UriSegmentError::NotValidUtf8)?;

View File

@ -1,6 +1,9 @@
# Changes # Changes
## Unreleased - 2022-xx-xx ## Unreleased - 2022-xx-xx
## 3.1.0 - 2023-01-21
- Minimum supported Rust version (MSRV) is now 1.59. - Minimum supported Rust version (MSRV) is now 1.59.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-http-test" name = "actix-http-test"
version = "3.0.0" version = "3.1.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Various helpers for Actix applications to use during testing" description = "Various helpers for Actix applications to use during testing"
keywords = ["http", "web", "framework", "async", "futures"] keywords = ["http", "web", "framework", "async", "futures"]
@ -37,9 +37,8 @@ actix-rt = "2.2"
actix-server = "2" actix-server = "2"
awc = { version = "3", default-features = false } awc = { version = "3", default-features = false }
base64 = "0.13"
bytes = "1" bytes = "1"
futures-core = { version = "0.3.7", default-features = false } futures-core = { version = "0.3.17", default-features = false }
http = "0.2.5" http = "0.2.5"
log = "0.4" log = "0.4"
socket2 = "0.4" socket2 = "0.4"
@ -48,7 +47,7 @@ serde_json = "1.0"
slab = "0.4" slab = "0.4"
serde_urlencoded = "0.7" serde_urlencoded = "0.7"
tls-openssl = { version = "0.10.9", package = "openssl", optional = true } tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
tokio = { version = "1.8.4", features = ["sync"] } tokio = { version = "1.18.5", features = ["sync"] }
[dev-dependencies] [dev-dependencies]
actix-web = { version = "4", default-features = false, features = ["cookies"] } actix-web = { version = "4", default-features = false, features = ["cookies"] }

View File

@ -3,11 +3,11 @@
> Various helpers for Actix applications to use during testing. > Various helpers for Actix applications to use during testing.
[![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test)
[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0)](https://docs.rs/actix-http-test/3.0.0) [![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.1.0)](https://docs.rs/actix-http-test/3.1.0)
![Version](https://img.shields.io/badge/rustc-1.59+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.59+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
<br> <br>
[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0) [![Dependency Status](https://deps.rs/crate/actix-http-test/3.1.0/status.svg)](https://deps.rs/crate/actix-http-test/3.1.0)
[![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test) [![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test)
[![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)

View File

@ -2,6 +2,7 @@
#![deny(rust_2018_idioms, nonstandard_style)] #![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)] #![warn(future_incompatible)]
#![allow(clippy::uninlined_format_args)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
@ -87,6 +88,7 @@ pub async fn test_server_with_addr<F: ServerServiceFactory<TcpStream>>(
// notify TestServer that server and system have shut down // notify TestServer that server and system have shut down
// all thread managed resources should be dropped at this point // all thread managed resources should be dropped at this point
#[allow(clippy::let_underscore_future)]
let _ = thread_stop_tx.send(()); let _ = thread_stop_tx.send(());
}); });
@ -294,6 +296,7 @@ impl Drop for TestServer {
// without needing to await anything // without needing to await anything
// signal server to stop // signal server to stop
#[allow(clippy::let_underscore_future)]
let _ = self.server.stop(true); let _ = self.server.stop(true);
// signal system to stop // signal system to stop

View File

@ -1,15 +1,39 @@
# Changes # Changes
## Unreleased - 2022-xx-xx ## Unreleased - 2022-xx-xx
## 3.3.0 - 2023-01-21
### Added ### Added
- Implement `MessageBody` for `Cow<'static, str>` and `Cow<'static, [u8]>`. [#2959]
- Implement `MessageBody` for `&mut B` where `B: MessageBody + Unpin`. [#2868] - Implement `MessageBody` for `&mut B` where `B: MessageBody + Unpin`. [#2868]
- Implement `MessageBody` for `Pin<B>` where `B::Target: MessageBody`. [#2868] - Implement `MessageBody` for `Pin<B>` where `B::Target: MessageBody`. [#2868]
- Automatic h2c detection via new service finalizer `HttpService::tcp_auto_h2c()`. [#2957]
- `HeaderMap::retain()`. [#2955]
- Header name constants in `header` module. [#2956] [#2968]
- `CACHE_STATUS`
- `CDN_CACHE_CONTROL`
- `CROSS_ORIGIN_EMBEDDER_POLICY`
- `CROSS_ORIGIN_OPENER_POLICY`
- `PERMISSIONS_POLICY`
- `X_FORWARDED_FOR`
- `X_FORWARDED_HOST`
- `X_FORWARDED_PROTO`
### Fixed
- Fix non-empty body of HTTP/2 HEAD responses. [#2920]
### Performance ### Performance
- Improve overall performance of operations on `Extensions`. [#2890] - Improve overall performance of operations on `Extensions`. [#2890]
[#2959]: https://github.com/actix/actix-web/pull/2959
[#2868]: https://github.com/actix/actix-web/pull/2868 [#2868]: https://github.com/actix/actix-web/pull/2868
[#2890]: https://github.com/actix/actix-web/pull/2890 [#2890]: https://github.com/actix/actix-web/pull/2890
[#2920]: https://github.com/actix/actix-web/pull/2920
[#2957]: https://github.com/actix/actix-web/pull/2957
[#2955]: https://github.com/actix/actix-web/pull/2955
[#2956]: https://github.com/actix/actix-web/pull/2956
[#2968]: https://github.com/actix/actix-web/pull/2968
## 3.2.2 - 2022-09-11 ## 3.2.2 - 2022-09-11
@ -657,7 +681,7 @@
- Reduce the level from `error` to `debug` for the log line that is emitted when a `500 Internal Server Error` is built using `HttpResponse::from_error`. [#2201] - Reduce the level from `error` to `debug` for the log line that is emitted when a `500 Internal Server Error` is built using `HttpResponse::from_error`. [#2201]
- `ResponseBuilder::message_body` now returns a `Result`. [#2201] - `ResponseBuilder::message_body` now returns a `Result`. [#2201]
- Remove `Unpin` bound on `ResponseBuilder::streaming`. [#2253] - Remove `Unpin` bound on `ResponseBuilder::streaming`. [#2253]
- `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuation parameter. [#2226] - `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuration parameter. [#2226]
### Removed ### Removed
- Stop re-exporting `http` crate's `HeaderMap` types in addition to ours. [#2171] - Stop re-exporting `http` crate's `HeaderMap` types in addition to ours. [#2171]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-http" name = "actix-http"
version = "3.2.2" version = "3.3.0"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>", "Rob Ede <robjtede@icloud.com>",
@ -67,7 +67,7 @@ bytes = "1"
bytestring = "1" bytestring = "1"
derive_more = "0.99.5" derive_more = "0.99.5"
encoding_rs = "0.8" encoding_rs = "0.8"
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
http = "0.2.5" http = "0.2.5"
httparse = "1.5.1" httparse = "1.5.1"
httpdate = "1.0.1" httpdate = "1.0.1"
@ -77,7 +77,7 @@ 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"
tokio = { version = "1.13.1", features = [] } tokio = { version = "1.18.5", features = [] }
tokio-util = { version = "0.7", features = ["io", "codec"] } tokio-util = { version = "0.7", features = ["io", "codec"] }
tracing = { version = "0.1.30", default-features = false, features = ["log"] } tracing = { version = "0.1.30", default-features = false, features = ["log"] }
@ -86,7 +86,7 @@ h2 = { version = "0.3.9", optional = true }
# websockets # websockets
local-channel = { version = "0.1", optional = true } local-channel = { version = "0.1", optional = true }
base64 = { version = "0.13", optional = true } base64 = { version = "0.21", optional = true }
rand = { version = "0.8", optional = true } rand = { version = "0.8", optional = true }
sha1 = { version = "0.10", optional = true } sha1 = { version = "0.10", optional = true }
@ -96,7 +96,7 @@ actix-tls = { version = "3", default-features = false, optional = true }
# compress-* # compress-*
brotli = { version = "3.3.3", optional = true } brotli = { version = "3.3.3", optional = true }
flate2 = { version = "1.0.13", optional = true } flate2 = { version = "1.0.13", optional = true }
zstd = { version = "0.11", optional = true } zstd = { version = "0.12", optional = true }
[dev-dependencies] [dev-dependencies]
actix-http-test = { version = "3", features = ["openssl"] } actix-http-test = { version = "3", features = ["openssl"] }
@ -105,9 +105,9 @@ actix-tls = { version = "3", features = ["openssl"] }
actix-web = "4" actix-web = "4"
async-stream = "0.3" async-stream = "0.3"
criterion = { version = "0.3", features = ["html_reports"] } criterion = { version = "0.4", features = ["html_reports"] }
env_logger = "0.9" env_logger = "0.9"
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
memchr = "2.4" memchr = "2.4"
once_cell = "1.9" once_cell = "1.9"
rcgen = "0.9" rcgen = "0.9"
@ -119,7 +119,7 @@ serde_json = "1.0"
static_assertions = "1" static_assertions = "1"
tls-openssl = { package = "openssl", version = "0.10.9" } tls-openssl = { package = "openssl", version = "0.10.9" }
tls-rustls = { package = "rustls", version = "0.20.0" } tls-rustls = { package = "rustls", version = "0.20.0" }
tokio = { version = "1.8.4", features = ["net", "rt", "macros"] } tokio = { version = "1.18.5", features = ["net", "rt", "macros"] }
[[example]] [[example]]
name = "ws" name = "ws"

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.2.2)](https://docs.rs/actix-http/3.2.2) [![Documentation](https://docs.rs/actix-http/badge.svg?version=3.3.0)](https://docs.rs/actix-http/3.3.0)
![Version](https://img.shields.io/badge/rustc-1.59+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.59+-ab6000.svg)
![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.2.2/status.svg)](https://deps.rs/crate/actix-http/3.2.2) [![dependency status](https://deps.rs/crate/actix-http/3.3.0/status.svg)](https://deps.rs/crate/actix-http/3.3.0)
[![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)

View File

@ -1,3 +1,5 @@
#![allow(clippy::uninlined_format_args)]
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
const CODES: &[u16] = &[0, 1000, 201, 800, 550]; const CODES: &[u16] = &[0, 1000, 201, 800, 550];

View File

@ -0,0 +1,29 @@
//! An example that supports automatic selection of plaintext h1/h2c connections.
//!
//! Notably, both the following commands will work.
//! ```console
//! $ curl --http1.1 'http://localhost:8080/'
//! $ curl --http2-prior-knowledge 'http://localhost:8080/'
//! ```
use std::{convert::Infallible, io};
use actix_http::{HttpService, Request, Response, StatusCode};
use actix_server::Server;
#[tokio::main(flavor = "current_thread")]
async fn main() -> io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
Server::build()
.bind("h2c-detect", ("127.0.0.1", 8080), || {
HttpService::build()
.finish(|_req: Request| async move {
Ok::<_, Infallible>(Response::build(StatusCode::OK).body("Hello!"))
})
.tcp_auto_h2c()
})?
.workers(2)
.run()
.await
}

View File

@ -120,7 +120,7 @@ pub trait MessageBody {
} }
mod foreign_impls { mod foreign_impls {
use std::ops::DerefMut; use std::{borrow::Cow, ops::DerefMut};
use super::*; use super::*;
@ -324,6 +324,39 @@ mod foreign_impls {
} }
} }
impl MessageBody for Cow<'static, [u8]> {
type Error = Infallible;
#[inline]
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
#[inline]
fn poll_next(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
let bytes = match mem::take(self.get_mut()) {
Cow::Borrowed(b) => Bytes::from_static(b),
Cow::Owned(b) => Bytes::from(b),
};
Poll::Ready(Some(Ok(bytes)))
}
}
#[inline]
fn try_into_bytes(self) -> Result<Bytes, Self> {
match self {
Cow::Borrowed(b) => Ok(Bytes::from_static(b)),
Cow::Owned(b) => Ok(Bytes::from(b)),
}
}
}
impl MessageBody for &'static str { impl MessageBody for &'static str {
type Error = Infallible; type Error = Infallible;
@ -379,6 +412,39 @@ mod foreign_impls {
} }
} }
impl MessageBody for Cow<'static, str> {
type Error = Infallible;
#[inline]
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
#[inline]
fn poll_next(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
let bytes = match mem::take(self.get_mut()) {
Cow::Borrowed(s) => Bytes::from_static(s.as_bytes()),
Cow::Owned(s) => Bytes::from(s.into_bytes()),
};
Poll::Ready(Some(Ok(bytes)))
}
}
#[inline]
fn try_into_bytes(self) -> Result<Bytes, Self> {
match self {
Cow::Borrowed(s) => Ok(Bytes::from_static(s.as_bytes())),
Cow::Owned(s) => Ok(Bytes::from(s.into_bytes())),
}
}
}
impl MessageBody for bytestring::ByteString { impl MessageBody for bytestring::ByteString {
type Error = Infallible; type Error = Infallible;

View File

@ -44,7 +44,7 @@ where
#[inline] #[inline]
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.size as u64) BodySize::Sized(self.size)
} }
/// Attempts to pull out the next value of the underlying [`Stream`]. /// Attempts to pull out the next value of the underlying [`Stream`].

View File

@ -186,7 +186,7 @@ where
self self
} }
/// Finish service configuration and create a HTTP Service for HTTP/1 protocol. /// Finish service configuration and create a service for the HTTP/1 protocol.
pub fn h1<F, B>(self, service: F) -> H1Service<T, S, B, X, U> pub fn h1<F, B>(self, service: F) -> H1Service<T, S, B, X, U>
where where
B: MessageBody, B: MessageBody,
@ -209,7 +209,7 @@ where
.on_connect_ext(self.on_connect_ext) .on_connect_ext(self.on_connect_ext)
} }
/// Finish service configuration and create a HTTP service for HTTP/2 protocol. /// Finish service configuration and create a service for the HTTP/2 protocol.
#[cfg(feature = "http2")] #[cfg(feature = "http2")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))] #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
pub fn h2<F, B>(self, service: F) -> crate::h2::H2Service<T, S, B> pub fn h2<F, B>(self, service: F) -> crate::h2::H2Service<T, S, B>

View File

@ -71,7 +71,7 @@ impl ChunkedState {
match size.checked_mul(radix) { match size.checked_mul(radix) {
Some(n) => { Some(n) => {
*size = n as u64; *size = n;
*size += rem as u64; *size += rem as u64;
Poll::Ready(Ok(ChunkedState::Size)) Poll::Ready(Ok(ChunkedState::Size))

View File

@ -64,7 +64,7 @@ fn drop_payload_service(
fn echo_payload_service() -> impl Service<Request, Response = Response<Bytes>, Error = Error> { fn echo_payload_service() -> impl Service<Request, Response = Response<Bytes>, Error = Error> {
fn_service(|mut req: Request| { fn_service(|mut req: Request| {
Box::pin(async move { Box::pin(async move {
use futures_util::stream::StreamExt as _; use futures_util::StreamExt as _;
let mut pl = req.take_payload(); let mut pl = req.take_payload();
let mut body = BytesMut::new(); let mut body = BytesMut::new();

View File

@ -450,7 +450,7 @@ impl TransferEncoding {
buf.extend_from_slice(&msg[..len as usize]); buf.extend_from_slice(&msg[..len as usize]);
*remaining -= len as u64; *remaining -= len;
Ok(*remaining == 0) Ok(*remaining == 0)
} else { } else {
Ok(true) Ok(true)

View File

@ -29,7 +29,7 @@ use crate::{
HeaderName, HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, UPGRADE, HeaderName, HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, UPGRADE,
}, },
service::HttpFlow, service::HttpFlow,
Extensions, OnConnectData, Payload, Request, Response, ResponseHead, Extensions, Method, OnConnectData, Payload, Request, Response, ResponseHead,
}; };
const CHUNK_SIZE: usize = 16_384; const CHUNK_SIZE: usize = 16_384;
@ -118,6 +118,7 @@ where
let payload = crate::h2::Payload::new(body); let payload = crate::h2::Payload::new(body);
let pl = Payload::H2 { payload }; let pl = Payload::H2 { payload };
let mut req = Request::with_payload(pl); let mut req = Request::with_payload(pl);
let head_req = parts.method == Method::HEAD;
let head = req.head_mut(); let head = req.head_mut();
head.uri = parts.uri; head.uri = parts.uri;
@ -135,10 +136,10 @@ where
actix_rt::spawn(async move { actix_rt::spawn(async move {
// resolve service call and send response. // resolve service call and send response.
let res = match fut.await { let res = match fut.await {
Ok(res) => handle_response(res.into(), tx, config).await, Ok(res) => handle_response(res.into(), tx, config, head_req).await,
Err(err) => { Err(err) => {
let res: Response<BoxBody> = err.into(); let res: Response<BoxBody> = err.into();
handle_response(res, tx, config).await handle_response(res, tx, config, head_req).await
} }
}; };
@ -206,6 +207,7 @@ async fn handle_response<B>(
res: Response<B>, res: Response<B>,
mut tx: SendResponse<Bytes>, mut tx: SendResponse<Bytes>,
config: ServiceConfig, config: ServiceConfig,
head_req: bool,
) -> Result<(), DispatchError> ) -> Result<(), DispatchError>
where where
B: MessageBody, B: MessageBody,
@ -215,14 +217,14 @@ where
// prepare response. // prepare response.
let mut size = body.size(); let mut size = body.size();
let res = prepare_response(config, res.head(), &mut size); let res = prepare_response(config, res.head(), &mut size);
let eof = size.is_eof(); let eof_or_head = size.is_eof() || head_req;
// send response head and return on eof. // send response head and return on eof.
let mut stream = tx let mut stream = tx
.send_response(res, eof) .send_response(res, eof_or_head)
.map_err(DispatchError::SendResponse)?; .map_err(DispatchError::SendResponse)?;
if eof { if eof_or_head {
return Ok(()); return Ok(());
} }

View File

@ -0,0 +1,51 @@
//! Common header names not defined in [`http`].
//!
//! Any headers added to this file will need to be re-exported from the list at `crate::headers`.
use http::header::HeaderName;
/// Response header field that indicates how caches have handled that response and its corresponding
/// request.
///
/// See [RFC 9211](https://www.rfc-editor.org/rfc/rfc9211) for full semantics.
pub const CACHE_STATUS: HeaderName = HeaderName::from_static("cache-status");
/// Response header field that allows origin servers to control the behavior of CDN caches
/// interposed between them and clients separately from other caches that might handle the response.
///
/// See [RFC 9213](https://www.rfc-editor.org/rfc/rfc9213) for full semantics.
pub const CDN_CACHE_CONTROL: HeaderName = HeaderName::from_static("cdn-cache-control");
/// Response header that prevents a document from loading any cross-origin resources that don't
/// explicitly grant the document permission (using [CORP] or [CORS]).
///
/// [CORP]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cross-Origin_Resource_Policy_(CORP)
/// [CORS]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
pub const CROSS_ORIGIN_EMBEDDER_POLICY: HeaderName =
HeaderName::from_static("cross-origin-embedder-policy");
/// Response header that allows you to ensure a top-level document does not share a browsing context
/// group with cross-origin documents.
pub const CROSS_ORIGIN_OPENER_POLICY: HeaderName =
HeaderName::from_static("cross-origin-opener-policy");
/// Response header that conveys a desire that the browser blocks no-cors cross-origin/cross-site
/// requests to the given resource.
pub const CROSS_ORIGIN_RESOURCE_POLICY: HeaderName =
HeaderName::from_static("cross-origin-resource-policy");
/// Response header that provides a mechanism to allow and deny the use of browser features in a
/// document or within any `<iframe>` elements in the document.
pub const PERMISSIONS_POLICY: HeaderName = HeaderName::from_static("permissions-policy");
/// Request header (de-facto standard) for identifying the originating IP address of a client
/// connecting to a web server through a proxy server.
pub const X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for");
/// Request header (de-facto standard) for identifying the original host requested by the client in
/// the `Host` HTTP request header.
pub const X_FORWARDED_HOST: HeaderName = HeaderName::from_static("x-forwarded-host");
/// Request header (de-facto standard) for identifying the protocol that a client used to connect to
/// your proxy or load balancer.
pub const X_FORWARDED_PROTO: HeaderName = HeaderName::from_static("x-forwarded-proto");

View File

@ -150,9 +150,7 @@ impl HeaderMap {
/// assert_eq!(map.len(), 3); /// assert_eq!(map.len(), 3);
/// ``` /// ```
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.inner self.inner.values().map(|vals| vals.len()).sum()
.iter()
.fold(0, |acc, (_, values)| acc + values.len())
} }
/// Returns the number of _keys_ stored in the map. /// Returns the number of _keys_ stored in the map.
@ -552,6 +550,39 @@ impl HeaderMap {
Keys(self.inner.keys()) Keys(self.inner.keys())
} }
/// Retains only the headers specified by the predicate.
///
/// In other words, removes all headers `(name, val)` for which `retain_fn(&name, &mut val)`
/// returns false.
///
/// The order in which headers are visited should be considered arbitrary.
///
/// # Examples
/// ```
/// # use actix_http::header::{self, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new();
///
/// map.append(header::HOST, HeaderValue::from_static("duck.com"));
/// map.append(header::SET_COOKIE, HeaderValue::from_static("one=1"));
/// map.append(header::SET_COOKIE, HeaderValue::from_static("two=2"));
///
/// map.retain(|name, val| val.as_bytes().starts_with(b"one"));
///
/// assert_eq!(map.len(), 1);
/// assert!(map.contains_key(&header::SET_COOKIE));
/// ```
pub fn retain<F>(&mut self, mut retain_fn: F)
where
F: FnMut(&HeaderName, &mut HeaderValue) -> bool,
{
self.inner.retain(|name, vals| {
vals.inner.retain(|val| retain_fn(name, val));
// invariant: make sure newly empty value lists are removed
!vals.is_empty()
})
}
/// Clears the map, returning all name-value sets as an iterator. /// Clears the map, returning all name-value sets as an iterator.
/// ///
/// Header names will only be yielded for the first value in each set. All items that are /// Header names will only be yielded for the first value in each set. All items that are
@ -943,6 +974,55 @@ mod tests {
assert!(map.is_empty()); assert!(map.is_empty());
} }
#[test]
fn retain() {
let mut map = HeaderMap::new();
map.append(header::LOCATION, HeaderValue::from_static("/test"));
map.append(header::HOST, HeaderValue::from_static("duck.com"));
map.append(header::COOKIE, HeaderValue::from_static("one=1"));
map.append(header::COOKIE, HeaderValue::from_static("two=2"));
assert_eq!(map.len(), 4);
// by value
map.retain(|_, val| !val.as_bytes().contains(&b'/'));
assert_eq!(map.len(), 3);
// by name
map.retain(|name, _| name.as_str() != "cookie");
assert_eq!(map.len(), 1);
// keep but mutate value
map.retain(|_, val| {
*val = HeaderValue::from_static("replaced");
true
});
assert_eq!(map.len(), 1);
assert_eq!(map.get("host").unwrap(), "replaced");
}
#[test]
fn retain_removes_empty_value_lists() {
let mut map = HeaderMap::with_capacity(3);
map.append(header::HOST, HeaderValue::from_static("duck.com"));
map.append(header::HOST, HeaderValue::from_static("duck.com"));
assert_eq!(map.len(), 2);
assert_eq!(map.len_keys(), 1);
assert_eq!(map.inner.len(), 1);
assert_eq!(map.capacity(), 3);
// remove everything
map.retain(|_n, _v| false);
assert_eq!(map.len(), 0);
assert_eq!(map.len_keys(), 0);
assert_eq!(map.inner.len(), 0);
assert_eq!(map.capacity(), 3);
}
#[test] #[test]
fn entries_into_iter() { fn entries_into_iter() {
let mut map = HeaderMap::new(); let mut map = HeaderMap::new();

View File

@ -1,14 +1,18 @@
//! Pre-defined `HeaderName`s, traits for parsing and conversion, and other header utility methods. //! Pre-defined `HeaderName`s, traits for parsing and conversion, and other header utility methods.
// declaring new header consts will yield this error
#![allow(clippy::declare_interior_mutable_const)]
use percent_encoding::{AsciiSet, CONTROLS}; use percent_encoding::{AsciiSet, CONTROLS};
// re-export from http except header map related items // re-export from http except header map related items
pub use http::header::{ pub use ::http::header::{
HeaderName, HeaderValue, InvalidHeaderName, InvalidHeaderValue, ToStrError, HeaderName, HeaderValue, InvalidHeaderName, InvalidHeaderValue, ToStrError,
}; };
// re-export const header names // re-export const header names, list is explicit so that any updates to `common` module do not
pub use http::header::{ // conflict with this set
pub use ::http::header::{
ACCEPT, ACCEPT_CHARSET, ACCEPT_ENCODING, ACCEPT_LANGUAGE, ACCEPT_RANGES, ACCEPT, ACCEPT_CHARSET, ACCEPT_ENCODING, ACCEPT_LANGUAGE, ACCEPT_RANGES,
ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS,
ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_EXPOSE_HEADERS, ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_EXPOSE_HEADERS,
@ -30,22 +34,30 @@ pub use http::header::{
use crate::{error::ParseError, HttpMessage}; use crate::{error::ParseError, HttpMessage};
mod as_name; mod as_name;
mod common;
mod into_pair; mod into_pair;
mod into_value; mod into_value;
pub mod map; pub mod map;
mod shared; mod shared;
mod utils; mod utils;
pub use self::as_name::AsHeaderName; pub use self::{
pub use self::into_pair::TryIntoHeaderPair; as_name::AsHeaderName,
pub use self::into_value::TryIntoHeaderValue; into_pair::TryIntoHeaderPair,
pub use self::map::HeaderMap; into_value::TryIntoHeaderValue,
pub use self::shared::{ map::HeaderMap,
parse_extended_value, q, Charset, ContentEncoding, ExtendedValue, HttpDate, LanguageTag, shared::{
Quality, QualityItem, parse_extended_value, q, Charset, ContentEncoding, ExtendedValue, HttpDate,
LanguageTag, Quality, QualityItem,
},
utils::{fmt_comma_delimited, from_comma_delimited, from_one_raw_str, http_percent_encode},
}; };
pub use self::utils::{
fmt_comma_delimited, from_comma_delimited, from_one_raw_str, http_percent_encode, // re-export list is explicit so that any updates to `http` do not conflict with this set
pub use self::common::{
CACHE_STATUS, CDN_CACHE_CONTROL, CROSS_ORIGIN_EMBEDDER_POLICY, CROSS_ORIGIN_OPENER_POLICY,
CROSS_ORIGIN_RESOURCE_POLICY, PERMISSIONS_POLICY, X_FORWARDED_FOR, X_FORWARDED_HOST,
X_FORWARDED_PROTO,
}; };
/// An interface for types that already represent a valid header. /// An interface for types that already represent a valid header.

View File

@ -21,7 +21,8 @@
#![allow( #![allow(
clippy::type_complexity, clippy::type_complexity,
clippy::too_many_arguments, clippy::too_many_arguments,
clippy::borrow_interior_mutable_const clippy::borrow_interior_mutable_const,
clippy::uninlined_format_args
)] )]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]

View File

@ -24,7 +24,39 @@ use crate::{
h1, ConnectCallback, OnConnectData, Protocol, Request, Response, ServiceConfig, h1, ConnectCallback, OnConnectData, Protocol, Request, Response, ServiceConfig,
}; };
/// A `ServiceFactory` for HTTP/1.1 or HTTP/2 protocol. /// A [`ServiceFactory`] for HTTP/1.1 and HTTP/2 connections.
///
/// Use [`build`](Self::build) to begin constructing service. Also see [`HttpServiceBuilder`].
///
/// # Automatic HTTP Version Selection
/// There are two ways to select the HTTP version of an incoming connection:
/// - One is to rely on the ALPN information that is provided when using a TLS (HTTPS); both
/// versions are supported automatically when using either of the `.rustls()` or `.openssl()`
/// finalizing methods.
/// - The other is to read the first few bytes of the TCP stream. This is the only viable approach
/// for supporting H2C, which allows the HTTP/2 protocol to work over plaintext connections. Use
/// the `.tcp_auto_h2c()` finalizing method to enable this behavior.
///
/// # Examples
/// ```
/// # use std::convert::Infallible;
/// use actix_http::{HttpService, Request, Response, StatusCode};
///
/// // this service would constructed in an actix_server::Server
///
/// # actix_rt::System::new().block_on(async {
/// HttpService::build()
/// // the builder finalizing method, other finalizers would not return an `HttpService`
/// .finish(|_req: Request| async move {
/// Ok::<_, Infallible>(
/// Response::build(StatusCode::OK).body("Hello!")
/// )
/// })
/// // the service finalizing method method
/// // you can use `.tcp_auto_h2c()`, `.rustls()`, or `.openssl()` instead of `.tcp()`
/// .tcp();
/// # })
/// ```
pub struct HttpService<T, S, B, X = h1::ExpectHandler, U = h1::UpgradeHandler> { pub struct HttpService<T, S, B, X = h1::ExpectHandler, U = h1::UpgradeHandler> {
srv: S, srv: S,
cfg: ServiceConfig, cfg: ServiceConfig,
@ -163,7 +195,9 @@ where
U::Error: fmt::Display + Into<Response<BoxBody>>, U::Error: fmt::Display + Into<Response<BoxBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
/// Create simple tcp stream service /// Creates TCP stream service from HTTP service.
///
/// The resulting service only supports HTTP/1.x.
pub fn tcp( pub fn tcp(
self, self,
) -> impl ServiceFactory< ) -> impl ServiceFactory<
@ -179,6 +213,42 @@ where
}) })
.and_then(self) .and_then(self)
} }
/// Creates TCP stream service from HTTP service that automatically selects HTTP/1.x or HTTP/2
/// on plaintext connections.
#[cfg(feature = "http2")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
pub fn tcp_auto_h2c(
self,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Response = (),
Error = DispatchError,
InitError = (),
> {
fn_service(move |io: TcpStream| async move {
// subset of HTTP/2 preface defined by RFC 9113 §3.4
// this subset was chosen to maximize likelihood that peeking only once will allow us to
// reliably determine version or else it should fallback to h1 and fail quickly if data
// on the wire is junk
const H2_PREFACE: &[u8] = b"PRI * HTTP/2";
let mut buf = [0; 12];
io.peek(&mut buf).await?;
let proto = if buf == H2_PREFACE {
Protocol::Http2
} else {
Protocol::Http1
};
let peer_addr = io.peer_addr().ok();
Ok((io, proto, peer_addr))
})
.and_then(self)
}
} }
/// Configuration options used when accepting TLS connection. /// Configuration options used when accepting TLS connection.

View File

@ -3,6 +3,7 @@ use std::{
fmt, fmt,
}; };
use base64::prelude::*;
use tracing::error; use tracing::error;
/// Operation codes defined in [RFC 6455 §11.8]. /// Operation codes defined in [RFC 6455 §11.8].
@ -244,7 +245,7 @@ pub fn hash_key(key: &[u8]) -> [u8; 28] {
}; };
let mut hash_b64 = [0; 28]; let mut hash_b64 = [0; 28];
let n = base64::encode_config_slice(hash, base64::STANDARD, &mut hash_b64); let n = BASE64_STANDARD.encode_slice(hash, &mut hash_b64).unwrap();
assert_eq!(n, 28); assert_eq!(n, 28);
hash_b64 hash_b64

View File

@ -1,4 +1,5 @@
#![cfg(feature = "openssl")] #![cfg(feature = "openssl")]
#![allow(clippy::uninlined_format_args)]
extern crate tls_openssl as openssl; extern crate tls_openssl as openssl;
@ -16,7 +17,7 @@ use actix_utils::future::{err, ok, ready};
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use derive_more::{Display, Error}; use derive_more::{Display, Error};
use futures_core::Stream; use futures_core::Stream;
use futures_util::stream::{once, StreamExt as _}; use futures_util::{stream::once, StreamExt as _};
use openssl::{ use openssl::{
pkey::PKey, pkey::PKey,
ssl::{SslAcceptor, SslMethod}, ssl::{SslAcceptor, SslMethod},

View File

@ -1,4 +1,5 @@
#![cfg(feature = "rustls")] #![cfg(feature = "rustls")]
#![allow(clippy::uninlined_format_args)]
extern crate tls_rustls as rustls; extern crate tls_rustls as rustls;

View File

@ -1,3 +1,5 @@
#![allow(clippy::uninlined_format_args)]
use std::{ use std::{
convert::Infallible, convert::Infallible,
io::{Read, Write}, io::{Read, Write},
@ -7,18 +9,15 @@ use std::{
use actix_http::{ use actix_http::{
body::{self, BodyStream, BoxBody, SizedStream}, body::{self, BodyStream, BoxBody, SizedStream},
header, Error, HttpService, KeepAlive, Request, Response, StatusCode, header, Error, HttpService, KeepAlive, Request, Response, StatusCode, Version,
}; };
use actix_http_test::test_server; use actix_http_test::test_server;
use actix_rt::time::sleep; use actix_rt::{net::TcpStream, time::sleep};
use actix_service::fn_service; use actix_service::fn_service;
use actix_utils::future::{err, ok, ready}; use actix_utils::future::{err, ok, ready};
use bytes::Bytes; use bytes::Bytes;
use derive_more::{Display, Error}; use derive_more::{Display, Error};
use futures_util::{ use futures_util::{stream::once, FutureExt as _, StreamExt as _};
stream::{once, StreamExt as _},
FutureExt as _,
};
use regex::Regex; use regex::Regex;
#[actix_rt::test] #[actix_rt::test]
@ -858,3 +857,44 @@ async fn not_modified_spec_h1() {
srv.stop().await; srv.stop().await;
} }
#[actix_rt::test]
async fn h2c_auto() {
let mut srv = test_server(|| {
HttpService::build()
.keep_alive(KeepAlive::Disabled)
.finish(|req: Request| {
let body = match req.version() {
Version::HTTP_11 => "h1",
Version::HTTP_2 => "h2",
_ => unreachable!(),
};
ok::<_, Infallible>(Response::ok().set_body(body))
})
.tcp_auto_h2c()
})
.await;
let req = srv.get("/");
assert_eq!(req.get_version(), &Version::HTTP_11);
let mut res = req.send().await.unwrap();
assert!(res.status().is_success());
assert_eq!(res.body().await.unwrap(), &b"h1"[..]);
// awc doesn't support forcing the version to http/2 so use h2 manually
let tcp = TcpStream::connect(srv.addr()).await.unwrap();
let (h2, connection) = h2::client::handshake(tcp).await.unwrap();
tokio::spawn(async move { connection.await.unwrap() });
let mut h2 = h2.ready().await.unwrap();
let request = ::http::Request::new(());
let (response, _) = h2.send_request(request, true).unwrap();
let (head, mut body) = response.await.unwrap().into_parts();
let body = body.data().await.unwrap().unwrap();
assert!(head.status.is_success());
assert_eq!(body, &b"h2"[..]);
srv.stop().await;
}

View File

@ -1,3 +1,5 @@
#![allow(clippy::uninlined_format_args)]
use std::{ use std::{
cell::Cell, cell::Cell,
convert::Infallible, convert::Infallible,

View File

@ -2,13 +2,18 @@
## Unreleased - 2022-xx-xx ## Unreleased - 2022-xx-xx
- Added `MultipartForm` typed data extractor. [#2883] - Added `MultipartForm` typed data extractor. [#2883]
- `Field::content_type()` now returns `Option<&mime::Mime>`. [#2880]
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
[#2880]: https://github.com/actix/actix-web/pull/2880 [#2880]: https://github.com/actix/actix-web/pull/2880
[#2883]: https://github.com/actix/actix-web/pull/2883 [#2883]: https://github.com/actix/actix-web/pull/2883
## 0.5.0 - 2023-01-21
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
- `Field::content_type()` now returns `Option<&mime::Mime>` [#2885]
[#2885]: https://github.com/actix/actix-web/pull/2885
## 0.4.0 - 2022-02-25 ## 0.4.0 - 2022-02-25
- No significant changes since `0.4.0-beta.13`. - No significant changes since `0.4.0-beta.13`.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-multipart" name = "actix-multipart"
version = "0.4.0" version = "0.5.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Multipart form support for Actix Web" description = "Multipart form support for Actix Web"
keywords = ["http", "web", "framework", "async", "futures"] keywords = ["http", "web", "framework", "async", "futures"]
@ -29,8 +29,7 @@ actix-web = { version = "4", default-features = false }
bytes = "1" bytes = "1"
derive_more = "0.99.5" derive_more = "0.99.5"
futures-core = "0.3.17" futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
httparse = "1.3" httparse = "1.3"
local-waker = "0.1" local-waker = "0.1"
log = "0.4" log = "0.4"
@ -49,5 +48,6 @@ actix-multipart-rfc7578 = "0.10"
actix-rt = "2.2" actix-rt = "2.2"
actix-test = "0.1.0" actix-test = "0.1.0"
awc = "3.0.1" awc = "3.0.1"
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
tokio = { version = "1.18.5", features = ["sync"] }
tokio-stream = "0.1" tokio-stream = "0.1"

View File

@ -3,11 +3,11 @@
> Multipart form support for Actix Web. > Multipart form support for Actix Web.
[![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart)
[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0)](https://docs.rs/actix-multipart/0.4.0) [![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.5.0)](https://docs.rs/actix-multipart/0.5.0)
![Version](https://img.shields.io/badge/rustc-1.59+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.59+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0) [![dependency status](https://deps.rs/crate/actix-multipart/0.5.0/status.svg)](https://deps.rs/crate/actix-multipart/0.5.0)
[![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart) [![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart)
[![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)

View File

@ -13,7 +13,7 @@ use crate::server::Multipart;
/// ``` /// ```
/// use actix_web::{web, HttpResponse, Error}; /// use actix_web::{web, HttpResponse, Error};
/// use actix_multipart::Multipart; /// use actix_multipart::Multipart;
/// use futures_util::stream::StreamExt as _; /// use futures_util::StreamExt as _;
/// ///
/// async fn index(mut payload: Multipart) -> Result<HttpResponse, Error> { /// async fn index(mut payload: Multipart) -> Result<HttpResponse, Error> {
/// // iterate over multipart stream /// // iterate over multipart stream

View File

@ -2,7 +2,7 @@
#![deny(rust_2018_idioms, nonstandard_style)] #![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)] #![warn(future_incompatible)]
#![allow(clippy::borrow_interior_mutable_const)] #![allow(clippy::borrow_interior_mutable_const, clippy::uninlined_format_args)]
#![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
// This allows us to use the actix_multipart_derive within this crate's tests // This allows us to use the actix_multipart_derive within this crate's tests

View File

@ -872,7 +872,7 @@ mod tests {
FromRequest, FromRequest,
}; };
use bytes::Bytes; use bytes::Bytes;
use futures_util::{future::lazy, StreamExt}; use futures_util::{future::lazy, StreamExt as _};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_stream::wrappers::UnboundedReceiverStream;

View File

@ -27,7 +27,7 @@ serde = "1"
tracing = { version = "0.1.30", default-features = false, features = ["log"] } tracing = { version = "0.1.30", default-features = false, features = ["log"] }
[dev-dependencies] [dev-dependencies]
criterion = { version = "0.3", features = ["html_reports"] } criterion = { version = "0.4", features = ["html_reports"] }
http = "0.2.5" http = "0.2.5"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
percent-encoding = "2.1" percent-encoding = "2.1"

View File

@ -1,3 +1,5 @@
#![allow(clippy::uninlined_format_args)]
use criterion::{black_box, criterion_group, criterion_main, Criterion}; use criterion::{black_box, criterion_group, criterion_main, Criterion};
use std::borrow::Cow; use std::borrow::Cow;

View File

@ -2,6 +2,7 @@
#![deny(rust_2018_idioms, nonstandard_style)] #![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)] #![warn(future_incompatible)]
#![allow(clippy::uninlined_format_args)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]

View File

@ -37,12 +37,12 @@ actix-utils = "3"
actix-web = { version = "4", default-features = false, features = ["cookies"] } actix-web = { version = "4", default-features = false, features = ["cookies"] }
awc = { version = "3", 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.17", default-features = false, features = ["std"] }
futures-util = { version = "0.3.7", default-features = false, features = [] } futures-util = { version = "0.3.17", default-features = false, features = [] }
log = "0.4" log = "0.4"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
serde_urlencoded = "0.7" serde_urlencoded = "0.7"
tls-openssl = { package = "openssl", version = "0.10.9", optional = true } tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
tls-rustls = { package = "rustls", version = "0.20.0", optional = true } tls-rustls = { package = "rustls", version = "0.20.0", optional = true }
tokio = { version = "1.8.4", features = ["sync"] } tokio = { version = "1.18.5", features = ["sync"] }

View File

@ -321,6 +321,7 @@ where
// all thread managed resources should be dropped at this point // all thread managed resources should be dropped at this point
}); });
#[allow(clippy::let_underscore_future)]
let _ = thread_stop_tx.send(()); let _ = thread_stop_tx.send(());
}); });
@ -567,6 +568,7 @@ impl Drop for TestServer {
// without needing to await anything // without needing to await anything
// signal server to stop // signal server to stop
#[allow(clippy::let_underscore_future)]
let _ = self.server.stop(true); let _ = self.server.stop(true);
// signal system to stop // signal system to stop

View File

@ -1,6 +1,9 @@
# Changes # Changes
## Unreleased - 2022-xx-xx ## Unreleased - 2022-xx-xx
## 4.2.0 - 2023-01-21
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-web-actors" name = "actix-web-actors"
version = "4.1.0" version = "4.2.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix actors support for Actix Web" description = "Actix actors support for Actix Web"
keywords = ["actix", "http", "web", "framework", "async"] keywords = ["actix", "http", "web", "framework", "async"]
@ -21,9 +21,9 @@ actix-web = { version = "4", default-features = false }
bytes = "1" bytes = "1"
bytestring = "1" bytestring = "1"
futures-core = { version = "0.3.7", default-features = false } futures-core = { version = "0.3.17", default-features = false }
pin-project-lite = "0.2" pin-project-lite = "0.2"
tokio = { version = "1.13.1", features = ["sync"] } tokio = { version = "1.18.5", features = ["sync"] }
tokio-util = { version = "0.7", features = ["codec"] } tokio-util = { version = "0.7", features = ["codec"] }
[dev-dependencies] [dev-dependencies]
@ -35,4 +35,4 @@ actix-web = { version = "4", features = ["macros"] }
mime = "0.3" mime = "0.3"
env_logger = "0.9" env_logger = "0.9"
futures-util = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.17", default-features = false }

View File

@ -3,11 +3,11 @@
> Actix actors support for Actix Web. > Actix actors support for Actix Web.
[![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors) [![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors)
[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.1.0)](https://docs.rs/actix-web-actors/4.1.0) [![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.2.0)](https://docs.rs/actix-web-actors/4.2.0)
![Version](https://img.shields.io/badge/rustc-1.59+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.59+-ab6000.svg)
![License](https://img.shields.io/crates/l/actix-web-actors.svg) ![License](https://img.shields.io/crates/l/actix-web-actors.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-web-actors/4.1.0/status.svg)](https://deps.rs/crate/actix-web-actors/4.1.0) [![dependency status](https://deps.rs/crate/actix-web-actors/4.2.0/status.svg)](https://deps.rs/crate/actix-web-actors/4.2.0)
[![Download](https://img.shields.io/crates/d/actix-web-actors.svg)](https://crates.io/crates/actix-web-actors) [![Download](https://img.shields.io/crates/d/actix-web-actors.svg)](https://crates.io/crates/actix-web-actors)
[![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)

View File

@ -57,6 +57,7 @@
#![deny(rust_2018_idioms, nonstandard_style)] #![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)] #![warn(future_incompatible)]
#![allow(clippy::uninlined_format_args)]
mod context; mod context;
pub mod ws; pub mod ws;

View File

@ -3,7 +3,7 @@ use actix_http::ws::Codec;
use actix_web::{web, App, HttpRequest}; use actix_web::{web, App, HttpRequest};
use actix_web_actors::ws; use actix_web_actors::ws;
use bytes::Bytes; use bytes::Bytes;
use futures_util::{SinkExt, StreamExt}; use futures_util::{SinkExt as _, StreamExt as _};
struct Ws; struct Ws;

View File

@ -1,6 +1,9 @@
# Changes # Changes
## Unreleased - 2022-xx-xx ## Unreleased - 2022-xx-xx
- Add support for Custom Methods with `#[route]` macro. [#2969]
[#2969]: https://github.com/actix/actix-web/pull/2969
## 4.1.0 - 2022-09-11 ## 4.1.0 - 2022-09-11

View File

@ -27,6 +27,6 @@ actix-test = "0.1"
actix-utils = "3" actix-utils = "3"
actix-web = "4" actix-web = "4"
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
trybuild = "1" trybuild = "1"
rustversion = "1" rustversion = "1"

View File

@ -105,7 +105,7 @@ mod route;
/// ``` /// ```
/// # use actix_web::HttpResponse; /// # use actix_web::HttpResponse;
/// # use actix_web_codegen::route; /// # use actix_web_codegen::route;
/// #[route("/test", method = "GET", method = "HEAD")] /// #[route("/test", method = "GET", method = "HEAD", method = "CUSTOM")]
/// async fn example() -> HttpResponse { /// async fn example() -> HttpResponse {
/// HttpResponse::Ok().finish() /// HttpResponse::Ok().finish()
/// } /// }

View File

@ -6,11 +6,11 @@ use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens, TokenStreamExt}; use quote::{quote, ToTokens, TokenStreamExt};
use syn::{parse_macro_input, AttributeArgs, Ident, LitStr, Meta, NestedMeta, Path}; use syn::{parse_macro_input, AttributeArgs, Ident, LitStr, Meta, NestedMeta, Path};
macro_rules! method_type { macro_rules! standard_method_type {
( (
$($variant:ident, $upper:ident, $lower:ident,)+ $($variant:ident, $upper:ident, $lower:ident,)+
) => { ) => {
#[derive(Debug, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum MethodType { pub enum MethodType {
$( $(
$variant, $variant,
@ -27,7 +27,7 @@ macro_rules! method_type {
fn parse(method: &str) -> Result<Self, String> { fn parse(method: &str) -> Result<Self, String> {
match method { match method {
$(stringify!($upper) => Ok(Self::$variant),)+ $(stringify!($upper) => Ok(Self::$variant),)+
_ => Err(format!("Unexpected HTTP method: `{}`", method)), _ => Err(format!("HTTP method must be uppercase: `{}`", method)),
} }
} }
@ -41,7 +41,7 @@ macro_rules! method_type {
}; };
} }
method_type! { standard_method_type! {
Get, GET, get, Get, GET, get,
Post, POST, post, Post, POST, post,
Put, PUT, put, Put, PUT, put,
@ -53,13 +53,6 @@ method_type! {
Patch, PATCH, patch, Patch, PATCH, patch,
} }
impl ToTokens for MethodType {
fn to_tokens(&self, stream: &mut TokenStream2) {
let ident = Ident::new(self.as_str(), Span::call_site());
stream.append(ident);
}
}
impl TryFrom<&syn::LitStr> for MethodType { impl TryFrom<&syn::LitStr> for MethodType {
type Error = syn::Error; type Error = syn::Error;
@ -69,12 +62,123 @@ impl TryFrom<&syn::LitStr> for MethodType {
} }
} }
impl ToTokens for MethodType {
fn to_tokens(&self, stream: &mut TokenStream2) {
let ident = Ident::new(self.as_str(), Span::call_site());
stream.append(ident);
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum MethodTypeExt {
Standard(MethodType),
Custom(LitStr),
}
impl MethodTypeExt {
/// Returns a single method guard token stream.
fn to_tokens_single_guard(&self) -> TokenStream2 {
match self {
MethodTypeExt::Standard(method) => {
quote! {
.guard(::actix_web::guard::#method())
}
}
MethodTypeExt::Custom(lit) => {
quote! {
.guard(::actix_web::guard::Method(
::actix_web::http::Method::from_bytes(#lit.as_bytes()).unwrap()
))
}
}
}
}
/// Returns a multi-method guard chain token stream.
fn to_tokens_multi_guard(&self, or_chain: Vec<impl ToTokens>) -> TokenStream2 {
debug_assert!(
!or_chain.is_empty(),
"empty or_chain passed to multi-guard constructor"
);
match self {
MethodTypeExt::Standard(method) => {
quote! {
.guard(
::actix_web::guard::Any(::actix_web::guard::#method())
#(#or_chain)*
)
}
}
MethodTypeExt::Custom(lit) => {
quote! {
.guard(
::actix_web::guard::Any(
::actix_web::guard::Method(
::actix_web::http::Method::from_bytes(#lit.as_bytes()).unwrap()
)
)
#(#or_chain)*
)
}
}
}
}
/// Returns a token stream containing the `.or` chain to be passed in to
/// [`MethodTypeExt::to_tokens_multi_guard()`].
fn to_tokens_multi_guard_or_chain(&self) -> TokenStream2 {
match self {
MethodTypeExt::Standard(method_type) => {
quote! {
.or(::actix_web::guard::#method_type())
}
}
MethodTypeExt::Custom(lit) => {
quote! {
.or(
::actix_web::guard::Method(
::actix_web::http::Method::from_bytes(#lit.as_bytes()).unwrap()
)
)
}
}
}
}
}
impl ToTokens for MethodTypeExt {
fn to_tokens(&self, stream: &mut TokenStream2) {
match self {
MethodTypeExt::Custom(lit_str) => {
let ident = Ident::new(lit_str.value().as_str(), Span::call_site());
stream.append(ident);
}
MethodTypeExt::Standard(method) => method.to_tokens(stream),
}
}
}
impl TryFrom<&syn::LitStr> for MethodTypeExt {
type Error = syn::Error;
fn try_from(value: &syn::LitStr) -> Result<Self, Self::Error> {
match MethodType::try_from(value) {
Ok(method) => Ok(MethodTypeExt::Standard(method)),
Err(_) if value.value().chars().all(|c| c.is_ascii_uppercase()) => {
Ok(MethodTypeExt::Custom(value.clone()))
}
Err(err) => Err(err),
}
}
}
struct Args { struct Args {
path: syn::LitStr, path: syn::LitStr,
resource_name: Option<syn::LitStr>, resource_name: Option<syn::LitStr>,
guards: Vec<Path>, guards: Vec<Path>,
wrappers: Vec<syn::Type>, wrappers: Vec<syn::Type>,
methods: HashSet<MethodType>, methods: HashSet<MethodTypeExt>,
} }
impl Args { impl Args {
@ -99,7 +203,7 @@ impl Args {
let is_route_macro = method.is_none(); let is_route_macro = method.is_none();
if let Some(method) = method { if let Some(method) = method {
methods.insert(method); methods.insert(MethodTypeExt::Standard(method));
} }
for arg in args { for arg in args {
@ -116,6 +220,7 @@ impl Args {
)); ));
} }
}, },
NestedMeta::Meta(syn::Meta::NameValue(nv)) => { NestedMeta::Meta(syn::Meta::NameValue(nv)) => {
if nv.path.is_ident("name") { if nv.path.is_ident("name") {
if let syn::Lit::Str(lit) = nv.lit { if let syn::Lit::Str(lit) = nv.lit {
@ -151,11 +256,10 @@ impl Args {
"HTTP method forbidden here. To handle multiple methods, use `route` instead", "HTTP method forbidden here. To handle multiple methods, use `route` instead",
)); ));
} else if let syn::Lit::Str(ref lit) = nv.lit { } else if let syn::Lit::Str(ref lit) = nv.lit {
let method = MethodType::try_from(lit)?; if !methods.insert(MethodTypeExt::try_from(lit)?) {
if !methods.insert(method) {
return Err(syn::Error::new_spanned( return Err(syn::Error::new_spanned(
&nv.lit, &nv.lit,
&format!( format!(
"HTTP method defined more than once: `{}`", "HTTP method defined more than once: `{}`",
lit.value() lit.value()
), ),
@ -174,11 +278,13 @@ impl Args {
)); ));
} }
} }
arg => { arg => {
return Err(syn::Error::new_spanned(arg, "Unknown attribute.")); return Err(syn::Error::new_spanned(arg, "Unknown attribute."));
} }
} }
} }
Ok(Args { Ok(Args {
path: path.unwrap(), path: path.unwrap(),
resource_name, resource_name,
@ -299,22 +405,19 @@ impl ToTokens for Route {
.map_or_else(|| name.to_string(), LitStr::value); .map_or_else(|| name.to_string(), LitStr::value);
let method_guards = { let method_guards = {
let mut others = methods.iter(); debug_assert!(!methods.is_empty(), "Args::methods should not be empty");
// unwrapping since length is checked to be at least one let mut others = methods.iter();
let first = others.next().unwrap(); let first = others.next().unwrap();
if methods.len() > 1 { if methods.len() > 1 {
quote! { let other_method_guards = others
.guard( .map(|method_ext| method_ext.to_tokens_multi_guard_or_chain())
::actix_web::guard::Any(::actix_web::guard::#first()) .collect();
#(.or(::actix_web::guard::#others()))*
) first.to_tokens_multi_guard(other_method_guards)
}
} else { } else {
quote! { first.to_tokens_single_guard()
.guard(::actix_web::guard::#first())
}
} }
}; };
@ -325,7 +428,6 @@ impl ToTokens for Route {
#(.guard(::actix_web::guard::fn_guard(#guards)))* #(.guard(::actix_web::guard::fn_guard(#guards)))*
#(.wrap(#wrappers))* #(.wrap(#wrappers))*
.to(#name); .to(#name);
::actix_web::dev::HttpServiceFactory::register(__resource, __config); ::actix_web::dev::HttpServiceFactory::register(__resource, __config);
} }
}) })

View File

@ -86,7 +86,18 @@ async fn get_param_test(_: web::Path<String>) -> impl Responder {
HttpResponse::Ok() HttpResponse::Ok()
} }
#[route("/multi", method = "GET", method = "POST", method = "HEAD")] #[route("/hello", method = "HELLO")]
async fn custom_route_test() -> impl Responder {
HttpResponse::Ok()
}
#[route(
"/multi",
method = "GET",
method = "POST",
method = "HEAD",
method = "HELLO"
)]
async fn route_test() -> impl Responder { async fn route_test() -> impl Responder {
HttpResponse::Ok() HttpResponse::Ok()
} }

View File

@ -9,9 +9,11 @@ fn compile_macros() {
t.pass("tests/trybuild/route-ok.rs"); t.pass("tests/trybuild/route-ok.rs");
t.compile_fail("tests/trybuild/route-missing-method-fail.rs"); t.compile_fail("tests/trybuild/route-missing-method-fail.rs");
t.compile_fail("tests/trybuild/route-duplicate-method-fail.rs"); t.compile_fail("tests/trybuild/route-duplicate-method-fail.rs");
t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs");
t.compile_fail("tests/trybuild/route-malformed-path-fail.rs"); t.compile_fail("tests/trybuild/route-malformed-path-fail.rs");
t.pass("tests/trybuild/route-custom-method.rs");
t.compile_fail("tests/trybuild/route-custom-lowercase.rs");
t.pass("tests/trybuild/routes-ok.rs"); t.pass("tests/trybuild/routes-ok.rs");
t.compile_fail("tests/trybuild/routes-missing-method-fail.rs"); t.compile_fail("tests/trybuild/routes-missing-method-fail.rs");
t.compile_fail("tests/trybuild/routes-missing-args-fail.rs"); t.compile_fail("tests/trybuild/routes-missing-args-fail.rs");

View File

@ -1,6 +1,8 @@
use actix_web_codegen::*; use actix_web_codegen::*;
use actix_web::http::Method;
use std::str::FromStr;
#[route("/", method="UNEXPECTED")] #[route("/", method = "hello")]
async fn index() -> String { async fn index() -> String {
"Hello World!".to_owned() "Hello World!".to_owned()
} }
@ -11,7 +13,7 @@ async fn main() {
let srv = actix_test::start(|| App::new().service(index)); let srv = actix_test::start(|| App::new().service(index));
let request = srv.get("/"); let request = srv.request(Method::from_str("hello").unwrap(), srv.url("/"));
let response = request.send().await.unwrap(); let response = request.send().await.unwrap();
assert!(response.status().is_success()); assert!(response.status().is_success());
} }

View File

@ -0,0 +1,19 @@
error: HTTP method must be uppercase: `hello`
--> tests/trybuild/route-custom-lowercase.rs:5:23
|
5 | #[route("/", method = "hello")]
| ^^^^^^^
error[E0277]: the trait bound `fn() -> impl std::future::Future<Output = String> {index}: HttpServiceFactory` is not satisfied
--> tests/trybuild/route-custom-lowercase.rs:14:55
|
14 | let srv = actix_test::start(|| App::new().service(index));
| ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future<Output = String> {index}`
| |
| required by a bound introduced by this call
|
note: required by a bound in `App::<T>::service`
--> $WORKSPACE/actix-web/src/app.rs
|
| F: HttpServiceFactory + 'static,
| ^^^^^^^^^^^^^^^^^^ required by this bound in `App::<T>::service`

View File

@ -0,0 +1,37 @@
use std::str::FromStr;
use actix_web::http::Method;
use actix_web_codegen::route;
#[route("/single", method = "CUSTOM")]
async fn index() -> String {
"Hello Single!".to_owned()
}
#[route("/multi", method = "GET", method = "CUSTOM")]
async fn custom() -> String {
"Hello Multi!".to_owned()
}
#[actix_web::main]
async fn main() {
use actix_web::App;
let srv = actix_test::start(|| App::new().service(index).service(custom));
let request = srv.request(Method::GET, srv.url("/"));
let response = request.send().await.unwrap();
assert!(response.status().is_client_error());
let request = srv.request(Method::from_str("CUSTOM").unwrap(), srv.url("/single"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(Method::GET, srv.url("/multi"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(Method::from_str("CUSTOM").unwrap(), srv.url("/multi"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
}

View File

@ -1,19 +0,0 @@
error: Unexpected HTTP method: `UNEXPECTED`
--> tests/trybuild/route-unexpected-method-fail.rs:3:21
|
3 | #[route("/", method="UNEXPECTED")]
| ^^^^^^^^^^^^
error[E0277]: the trait bound `fn() -> impl std::future::Future<Output = String> {index}: HttpServiceFactory` is not satisfied
--> tests/trybuild/route-unexpected-method-fail.rs:12:55
|
12 | let srv = actix_test::start(|| App::new().service(index));
| ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future<Output = String> {index}`
| |
| required by a bound introduced by this call
|
note: required by a bound in `App::<T>::service`
--> $WORKSPACE/actix-web/src/app.rs
|
| F: HttpServiceFactory + 'static,
| ^^^^^^^^^^^^^^^^^^ required by this bound in `App::<T>::service`

View File

@ -1,18 +1,26 @@
# Changelog # Changelog
## Unreleased - 2022-xx-xx ## Unreleased - 2022-xx-xx
## 4.3.0 - 2023-01-21
### Added ### Added
- Add `ContentDisposition::attachment` constructor. [#2867] - Add `ContentDisposition::attachment()` constructor. [#2867]
- Add `ErrorHandlers::default_handler()` (as well as `default_handler_{server, client}()`) to make registering handlers for groups of response statuses easier. [#2784] - Add `ErrorHandlers::default_handler()` (as well as `default_handler_{server, client}()`) to make registering handlers for groups of response statuses easier. [#2784]
- Add `Logger::custom_response_replace()`. [#2631] - Add `Logger::custom_response_replace()`. [#2631]
- Add rudimentary redirection service at `web::redirect()` / `web::Redirect`. [#1961] - Add rudimentary redirection service at `web::redirect()` / `web::Redirect`. [#1961]
- Add `guard::Acceptable` for matching against `Accept` header mime types. [#2265] - Add `guard::Acceptable` for matching against `Accept` header MIME types. [#2265]
- Add fallible versions of `test` helpers: `try_call_service()`, `try_call_and_read_body_json()`, `try_read_body()`, and `try_read_body_json()`. [#2961]
### Fixed
- Add `Allow` header to `Resource`'s default responses when no routes are matched. [#2949]
[#1961]: https://github.com/actix/actix-web/pull/1961 [#1961]: https://github.com/actix/actix-web/pull/1961
[#2265]: https://github.com/actix/actix-web/pull/2265 [#2265]: https://github.com/actix/actix-web/pull/2265
[#2631]: https://github.com/actix/actix-web/pull/2631 [#2631]: https://github.com/actix/actix-web/pull/2631
[#2784]: https://github.com/actix/actix-web/pull/2784 [#2784]: https://github.com/actix/actix-web/pull/2784
[#2867]: https://github.com/actix/actix-web/pull/2867 [#2867]: https://github.com/actix/actix-web/pull/2867
[#2949]: https://github.com/actix/actix-web/pull/2949
[#2961]: https://github.com/actix/actix-web/pull/2961
## 4.2.1 - 2022-09-12 ## 4.2.1 - 2022-09-12

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-web" name = "actix-web"
version = "4.2.1" version = "4.3.0"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>", "Rob Ede <robjtede@icloud.com>",
@ -38,10 +38,7 @@ compress-gzip = ["actix-http/compress-gzip", "__compress"]
compress-zstd = ["actix-http/compress-zstd", "__compress"] compress-zstd = ["actix-http/compress-zstd", "__compress"]
# Routing and runtime proc macros # Routing and runtime proc macros
macros = [ macros = ["actix-macros", "actix-web-codegen"]
"actix-macros",
"actix-web-codegen",
]
# Cookies support # Cookies support
cookies = ["cookie"] cookies = ["cookie"]
@ -71,7 +68,7 @@ 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.2.2", features = ["http2", "ws"] } actix-http = { version = "3.3", features = ["http2", "ws"] }
actix-router = "0.5" actix-router = "0.5"
actix-web-codegen = { version = "4.1", optional = true } actix-web-codegen = { version = "4.1", optional = true }
@ -82,8 +79,8 @@ cfg-if = "1"
cookie = { version = "0.16", features = ["percent-encode"], optional = true } cookie = { version = "0.16", features = ["percent-encode"], optional = true }
derive_more = "0.99.8" derive_more = "0.99.8"
encoding_rs = "0.8" encoding_rs = "0.8"
futures-core = { version = "0.3.7", default-features = false } futures-core = { version = "0.3.17", default-features = false }
futures-util = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.17", default-features = false }
http = "0.2.8" http = "0.2.8"
itoa = "1" itoa = "1"
language-tags = "0.3" language-tags = "0.3"
@ -107,10 +104,10 @@ awc = { version = "3", features = ["openssl"] }
brotli = "3.3.3" brotli = "3.3.3"
const-str = "0.4" const-str = "0.4"
criterion = { version = "0.3", features = ["html_reports"] } criterion = { version = "0.4", features = ["html_reports"] }
env_logger = "0.9" env_logger = "0.9"
flate2 = "1.0.13" flate2 = "1.0.13"
futures-util = { version = "0.3.7", default-features = false, features = ["std"] } futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
rand = "0.8" rand = "0.8"
rcgen = "0.9" rcgen = "0.9"
rustls-pemfile = "1" rustls-pemfile = "1"
@ -118,8 +115,8 @@ serde = { version = "1.0", features = ["derive"] }
static_assertions = "1" static_assertions = "1"
tls-openssl = { package = "openssl", version = "0.10.9" } tls-openssl = { package = "openssl", version = "0.10.9" }
tls-rustls = { package = "rustls", version = "0.20.0" } tls-rustls = { package = "rustls", version = "0.20.0" }
tokio = { version = "1.13.1", features = ["rt-multi-thread", "macros"] } tokio = { version = "1.18.5", features = ["rt-multi-thread", "macros"] }
zstd = "0.11" zstd = "0.12"
[[test]] [[test]]
name = "test_server" name = "test_server"

View File

@ -14,7 +14,7 @@
- `actix_http_test::TestServer` moved to `actix_web::test` module. To start - `actix_http_test::TestServer` moved to `actix_web::test` module. To start
test server use `test::start()` or `test_start_with_config()` methods test server use `test::start()` or `test_start_with_config()` methods
- `ResponseError` trait has been reafctored. `ResponseError::error_response()` renders - `ResponseError` trait has been refactored. `ResponseError::error_response()` renders
http response. http response.
- Feature `rust-tls` renamed to `rustls` - Feature `rust-tls` renamed to `rustls`

View File

@ -6,10 +6,10 @@
<p> <p>
[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) [![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web)
[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.2.1)](https://docs.rs/actix-web/4.2.1) [![Documentation](https://docs.rs/actix-web/badge.svg?version=4.3.0)](https://docs.rs/actix-web/4.3.0)
![MSRV](https://img.shields.io/badge/rustc-1.59+-ab6000.svg) ![MSRV](https://img.shields.io/badge/rustc-1.59+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg)
[![Dependency Status](https://deps.rs/crate/actix-web/4.2.1/status.svg)](https://deps.rs/crate/actix-web/4.2.1) [![Dependency Status](https://deps.rs/crate/actix-web/4.3.0/status.svg)](https://deps.rs/crate/actix-web/4.3.0)
<br /> <br />
[![CI](https://github.com/actix/actix-web/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-web/actions/workflows/ci.yml) [![CI](https://github.com/actix/actix-web/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-web/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web)

View File

@ -1,3 +1,5 @@
#![allow(clippy::uninlined_format_args)]
use actix_web::{web, App, HttpResponse}; use actix_web::{web, App, HttpResponse};
use awc::Client; use awc::Client;
use criterion::{criterion_group, criterion_main, Criterion}; use criterion::{criterion_group, criterion_main, Criterion};

View File

@ -1,3 +1,5 @@
#![allow(clippy::uninlined_format_args)]
use actix_web::{get, middleware, web, App, HttpRequest, HttpResponse, HttpServer}; use actix_web::{get, middleware, web, App, HttpRequest, HttpResponse, HttpServer};
#[get("/resource1/{name}/index.html")] #[get("/resource1/{name}/index.html")]

View File

@ -1,3 +1,5 @@
#![allow(clippy::uninlined_format_args)]
use actix_web::{middleware, rt, web, App, HttpRequest, HttpServer}; use actix_web::{middleware, rt, web, App, HttpRequest, HttpServer};
async fn index(req: HttpRequest) -> &'static str { async fn index(req: HttpRequest) -> &'static str {

View File

@ -4,6 +4,8 @@
//! For an example of extracting a client TLS certificate, see: //! For an example of extracting a client TLS certificate, see:
//! <https://github.com/actix/examples/tree/master/https-tls/rustls-client-cert> //! <https://github.com/actix/examples/tree/master/https-tls/rustls-client-cert>
#![allow(clippy::uninlined_format_args)]
use std::{any::Any, io, net::SocketAddr}; use std::{any::Any, io, net::SocketAddr};
use actix_web::{ use actix_web::{

View File

@ -1,3 +1,5 @@
#![allow(clippy::uninlined_format_args)]
use actix_web::{get, web, HttpRequest}; use actix_web::{get, web, HttpRequest};
#[cfg(unix)] #[cfg(unix)]
use actix_web::{middleware, App, Error, HttpResponse, HttpServer}; use actix_web::{middleware, App, Error, HttpResponse, HttpServer};

View File

@ -5,7 +5,7 @@ use actix_service::{
apply, apply_fn_factory, boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt, apply, apply_fn_factory, boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt,
Transform, Transform,
}; };
use futures_util::future::FutureExt as _; use futures_util::FutureExt as _;
use crate::{ use crate::{
app_service::{AppEntry, AppInit, AppRoutingFactory}, app_service::{AppEntry, AppInit, AppRoutingFactory},
@ -712,6 +712,7 @@ mod tests {
.route("/", web::to(|| async { "hello" })) .route("/", web::to(|| async { "hello" }))
} }
#[allow(clippy::let_underscore_future)]
let _ = init_service(my_app()); let _ = init_service(my_app());
} }
} }

View File

@ -21,7 +21,7 @@ use crate::{
Error, HttpResponse, Error, HttpResponse,
}; };
/// Service factory to convert `Request` to a `ServiceRequest<S>`. /// Service factory to convert [`Request`] to a [`ServiceRequest<S>`].
/// ///
/// It also executes data factories. /// It also executes data factories.
pub struct AppInit<T, B> pub struct AppInit<T, B>
@ -155,7 +155,7 @@ where
app_state: Rc<AppInitServiceState>, app_state: Rc<AppInitServiceState>,
} }
/// A collection of [`AppInitService`] state that shared across `HttpRequest`s. /// A collection of state for [`AppInitService`] that is shared across [`HttpRequest`]s.
pub(crate) struct AppInitServiceState { pub(crate) struct AppInitServiceState {
rmap: Rc<ResourceMap>, rmap: Rc<ResourceMap>,
config: AppConfig, config: AppConfig,
@ -163,6 +163,7 @@ pub(crate) struct AppInitServiceState {
} }
impl AppInitServiceState { impl AppInitServiceState {
/// Constructs state collection from resource map and app config.
pub(crate) fn new(rmap: Rc<ResourceMap>, config: AppConfig) -> Rc<Self> { pub(crate) fn new(rmap: Rc<ResourceMap>, config: AppConfig) -> Rc<Self> {
Rc::new(AppInitServiceState { Rc::new(AppInitServiceState {
rmap, rmap,
@ -171,16 +172,19 @@ impl AppInitServiceState {
}) })
} }
/// Returns a reference to the application's resource map.
#[inline] #[inline]
pub(crate) fn rmap(&self) -> &ResourceMap { pub(crate) fn rmap(&self) -> &ResourceMap {
&self.rmap &self.rmap
} }
/// Returns a reference to the application's configuration.
#[inline] #[inline]
pub(crate) fn config(&self) -> &AppConfig { pub(crate) fn config(&self) -> &AppConfig {
&self.config &self.config
} }
/// Returns a reference to the application's request pool.
#[inline] #[inline]
pub(crate) fn pool(&self) -> &HttpRequestPool { pub(crate) fn pool(&self) -> &HttpRequestPool {
&self.pool &self.pool

View File

@ -141,7 +141,7 @@ impl AppConfig {
self.secure self.secure
} }
/// Returns the socket address of the local half of this TCP connection /// Returns the socket address of the local half of this TCP connection.
pub fn local_addr(&self) -> SocketAddr { pub fn local_addr(&self) -> SocketAddr {
self.addr self.addr
} }

209
actix-web/src/guard/host.rs Normal file
View File

@ -0,0 +1,209 @@
use actix_http::{header, uri::Uri, RequestHead};
use super::{Guard, GuardContext};
/// Creates a guard that matches requests targetting a specific host.
///
/// # Matching Host
/// This guard will:
/// - match against the `Host` header, if present;
/// - fall-back to matching against the request target's host, if present;
/// - return false if host cannot be determined;
///
/// # Matching Scheme
/// Optionally, this guard can match against the host's scheme. Set the scheme for matching using
/// `Host(host).scheme(protocol)`. If the request's scheme cannot be determined, it will not prevent
/// the guard from matching successfully.
///
/// # Examples
/// The `Host` guard can be used to set up a form of [virtual hosting] within a single app.
/// Overlapping scope prefixes are usually discouraged, but when combined with non-overlapping guard
/// definitions they become safe to use in this way. Without these host guards, only routes under
/// the first-to-be-defined scope would be accessible. You can test this locally using `127.0.0.1`
/// and `localhost` as the `Host` guards.
/// ```
/// use actix_web::{web, http::Method, guard, App, HttpResponse};
///
/// App::new()
/// .service(
/// web::scope("")
/// .guard(guard::Host("www.rust-lang.org"))
/// .default_service(web::to(|| async {
/// HttpResponse::Ok().body("marketing site")
/// })),
/// )
/// .service(
/// web::scope("")
/// .guard(guard::Host("play.rust-lang.org"))
/// .default_service(web::to(|| async {
/// HttpResponse::Ok().body("playground frontend")
/// })),
/// );
/// ```
///
/// The example below additionally guards on the host URI's scheme. This could allow routing to
/// different handlers for `http:` vs `https:` visitors; to redirect, for example.
/// ```
/// use actix_web::{web, guard::Host, HttpResponse};
///
/// web::scope("/admin")
/// .guard(Host("admin.rust-lang.org").scheme("https"))
/// .default_service(web::to(|| async {
/// HttpResponse::Ok().body("admin connection is secure")
/// }));
/// ```
///
/// [virtual hosting]: https://en.wikipedia.org/wiki/Virtual_hosting
#[allow(non_snake_case)]
pub fn Host(host: impl AsRef<str>) -> HostGuard {
HostGuard {
host: host.as_ref().to_string(),
scheme: None,
}
}
fn get_host_uri(req: &RequestHead) -> Option<Uri> {
req.headers
.get(header::HOST)
.and_then(|host_value| host_value.to_str().ok())
.or_else(|| req.uri.host())
.and_then(|host| host.parse().ok())
}
#[doc(hidden)]
pub struct HostGuard {
host: String,
scheme: Option<String>,
}
impl HostGuard {
/// Set request scheme to match
pub fn scheme<H: AsRef<str>>(mut self, scheme: H) -> HostGuard {
self.scheme = Some(scheme.as_ref().to_string());
self
}
}
impl Guard for HostGuard {
fn check(&self, ctx: &GuardContext<'_>) -> bool {
// parse host URI from header or request target
let req_host_uri = match get_host_uri(ctx.head()) {
Some(uri) => uri,
// no match if host cannot be determined
None => return false,
};
match req_host_uri.host() {
// fall through to scheme checks
Some(uri_host) if self.host == uri_host => {}
// Either:
// - request's host does not match guard's host;
// - It was possible that the parsed URI from request target did not contain a host.
_ => return false,
}
if let Some(ref scheme) = self.scheme {
if let Some(ref req_host_uri_scheme) = req_host_uri.scheme_str() {
return scheme == req_host_uri_scheme;
}
// TODO: is this the correct behavior?
// falls through if scheme cannot be determined
}
// all conditions passed
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::TestRequest;
#[test]
fn host_from_header() {
let req = TestRequest::default()
.insert_header((
header::HOST,
header::HeaderValue::from_static("www.rust-lang.org"),
))
.to_srv_request();
let host = Host("www.rust-lang.org");
assert!(host.check(&req.guard_ctx()));
let host = Host("www.rust-lang.org").scheme("https");
assert!(host.check(&req.guard_ctx()));
let host = Host("blog.rust-lang.org");
assert!(!host.check(&req.guard_ctx()));
let host = Host("blog.rust-lang.org").scheme("https");
assert!(!host.check(&req.guard_ctx()));
let host = Host("crates.io");
assert!(!host.check(&req.guard_ctx()));
let host = Host("localhost");
assert!(!host.check(&req.guard_ctx()));
}
#[test]
fn host_without_header() {
let req = TestRequest::default()
.uri("www.rust-lang.org")
.to_srv_request();
let host = Host("www.rust-lang.org");
assert!(host.check(&req.guard_ctx()));
let host = Host("www.rust-lang.org").scheme("https");
assert!(host.check(&req.guard_ctx()));
let host = Host("blog.rust-lang.org");
assert!(!host.check(&req.guard_ctx()));
let host = Host("blog.rust-lang.org").scheme("https");
assert!(!host.check(&req.guard_ctx()));
let host = Host("crates.io");
assert!(!host.check(&req.guard_ctx()));
let host = Host("localhost");
assert!(!host.check(&req.guard_ctx()));
}
#[test]
fn host_scheme() {
let req = TestRequest::default()
.insert_header((
header::HOST,
header::HeaderValue::from_static("https://www.rust-lang.org"),
))
.to_srv_request();
let host = Host("www.rust-lang.org").scheme("https");
assert!(host.check(&req.guard_ctx()));
let host = Host("www.rust-lang.org");
assert!(host.check(&req.guard_ctx()));
let host = Host("www.rust-lang.org").scheme("http");
assert!(!host.check(&req.guard_ctx()));
let host = Host("blog.rust-lang.org");
assert!(!host.check(&req.guard_ctx()));
let host = Host("blog.rust-lang.org").scheme("https");
assert!(!host.check(&req.guard_ctx()));
let host = Host("crates.io").scheme("https");
assert!(!host.check(&req.guard_ctx()));
let host = Host("localhost");
assert!(!host.check(&req.guard_ctx()));
}
}

View File

@ -52,12 +52,15 @@ use std::{
rc::Rc, rc::Rc,
}; };
use actix_http::{header, uri::Uri, Extensions, Method as HttpMethod, RequestHead}; use actix_http::{header, Extensions, Method as HttpMethod, RequestHead};
use crate::{http::header::Header, service::ServiceRequest, HttpMessage as _}; use crate::{http::header::Header, service::ServiceRequest, HttpMessage as _};
mod acceptable; mod acceptable;
mod host;
pub use self::acceptable::Acceptable; pub use self::acceptable::Acceptable;
pub use self::host::{Host, HostGuard};
/// Provides access to request parts that are useful during routing. /// Provides access to request parts that are useful during routing.
#[derive(Debug)] #[derive(Debug)]
@ -286,11 +289,25 @@ pub fn Method(method: HttpMethod) -> impl Guard {
MethodGuard(method) MethodGuard(method)
} }
#[derive(Debug, Clone)]
pub(crate) struct RegisteredMethods(pub(crate) Vec<HttpMethod>);
/// HTTP method guard. /// HTTP method guard.
struct MethodGuard(HttpMethod); #[derive(Debug)]
pub(crate) struct MethodGuard(HttpMethod);
impl Guard for MethodGuard { impl Guard for MethodGuard {
fn check(&self, ctx: &GuardContext<'_>) -> bool { fn check(&self, ctx: &GuardContext<'_>) -> bool {
let registered = ctx.req_data_mut().remove::<RegisteredMethods>();
if let Some(mut methods) = registered {
methods.0.push(self.0.clone());
ctx.req_data_mut().insert(methods);
} else {
ctx.req_data_mut()
.insert(RegisteredMethods(vec![self.0.clone()]));
}
ctx.head().method == self.0 ctx.head().method == self.0
} }
} }
@ -357,124 +374,6 @@ impl Guard for HeaderGuard {
} }
} }
/// Creates a guard that matches requests targetting a specific host.
///
/// # Matching Host
/// This guard will:
/// - match against the `Host` header, if present;
/// - fall-back to matching against the request target's host, if present;
/// - return false if host cannot be determined;
///
/// # Matching Scheme
/// Optionally, this guard can match against the host's scheme. Set the scheme for matching using
/// `Host(host).scheme(protocol)`. If the request's scheme cannot be determined, it will not prevent
/// the guard from matching successfully.
///
/// # Examples
/// The [module-level documentation](self) has an example of virtual hosting using `Host` guards.
///
/// The example below additionally guards on the host URI's scheme. This could allow routing to
/// different handlers for `http:` vs `https:` visitors; to redirect, for example.
/// ```
/// use actix_web::{web, guard::Host, HttpResponse};
///
/// web::scope("/admin")
/// .guard(Host("admin.rust-lang.org").scheme("https"))
/// .default_service(web::to(|| async {
/// HttpResponse::Ok().body("admin connection is secure")
/// }));
/// ```
///
/// The `Host` guard can be used to set up some form of [virtual hosting] within a single app.
/// Overlapping scope prefixes are usually discouraged, but when combined with non-overlapping guard
/// definitions they become safe to use in this way. Without these host guards, only routes under
/// the first-to-be-defined scope would be accessible. You can test this locally using `127.0.0.1`
/// and `localhost` as the `Host` guards.
/// ```
/// use actix_web::{web, http::Method, guard, App, HttpResponse};
///
/// App::new()
/// .service(
/// web::scope("")
/// .guard(guard::Host("www.rust-lang.org"))
/// .default_service(web::to(|| async {
/// HttpResponse::Ok().body("marketing site")
/// })),
/// )
/// .service(
/// web::scope("")
/// .guard(guard::Host("play.rust-lang.org"))
/// .default_service(web::to(|| async {
/// HttpResponse::Ok().body("playground frontend")
/// })),
/// );
/// ```
///
/// [virtual hosting]: https://en.wikipedia.org/wiki/Virtual_hosting
#[allow(non_snake_case)]
pub fn Host(host: impl AsRef<str>) -> HostGuard {
HostGuard {
host: host.as_ref().to_string(),
scheme: None,
}
}
fn get_host_uri(req: &RequestHead) -> Option<Uri> {
req.headers
.get(header::HOST)
.and_then(|host_value| host_value.to_str().ok())
.or_else(|| req.uri.host())
.and_then(|host| host.parse().ok())
}
#[doc(hidden)]
pub struct HostGuard {
host: String,
scheme: Option<String>,
}
impl HostGuard {
/// Set request scheme to match
pub fn scheme<H: AsRef<str>>(mut self, scheme: H) -> HostGuard {
self.scheme = Some(scheme.as_ref().to_string());
self
}
}
impl Guard for HostGuard {
fn check(&self, ctx: &GuardContext<'_>) -> bool {
// parse host URI from header or request target
let req_host_uri = match get_host_uri(ctx.head()) {
Some(uri) => uri,
// no match if host cannot be determined
None => return false,
};
match req_host_uri.host() {
// fall through to scheme checks
Some(uri_host) if self.host == uri_host => {}
// Either:
// - request's host does not match guard's host;
// - It was possible that the parsed URI from request target did not contain a host.
_ => return false,
}
if let Some(ref scheme) = self.scheme {
if let Some(ref req_host_uri_scheme) = req_host_uri.scheme_str() {
return scheme == req_host_uri_scheme;
}
// TODO: is this the correct behavior?
// falls through if scheme cannot be determined
}
// all conditions passed
true
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use actix_http::{header, Method}; use actix_http::{header, Method};
@ -501,90 +400,6 @@ mod tests {
assert!(!hdr.check(&req.guard_ctx())); assert!(!hdr.check(&req.guard_ctx()));
} }
#[test]
fn host_from_header() {
let req = TestRequest::default()
.insert_header((
header::HOST,
header::HeaderValue::from_static("www.rust-lang.org"),
))
.to_srv_request();
let host = Host("www.rust-lang.org");
assert!(host.check(&req.guard_ctx()));
let host = Host("www.rust-lang.org").scheme("https");
assert!(host.check(&req.guard_ctx()));
let host = Host("blog.rust-lang.org");
assert!(!host.check(&req.guard_ctx()));
let host = Host("blog.rust-lang.org").scheme("https");
assert!(!host.check(&req.guard_ctx()));
let host = Host("crates.io");
assert!(!host.check(&req.guard_ctx()));
let host = Host("localhost");
assert!(!host.check(&req.guard_ctx()));
}
#[test]
fn host_without_header() {
let req = TestRequest::default()
.uri("www.rust-lang.org")
.to_srv_request();
let host = Host("www.rust-lang.org");
assert!(host.check(&req.guard_ctx()));
let host = Host("www.rust-lang.org").scheme("https");
assert!(host.check(&req.guard_ctx()));
let host = Host("blog.rust-lang.org");
assert!(!host.check(&req.guard_ctx()));
let host = Host("blog.rust-lang.org").scheme("https");
assert!(!host.check(&req.guard_ctx()));
let host = Host("crates.io");
assert!(!host.check(&req.guard_ctx()));
let host = Host("localhost");
assert!(!host.check(&req.guard_ctx()));
}
#[test]
fn host_scheme() {
let req = TestRequest::default()
.insert_header((
header::HOST,
header::HeaderValue::from_static("https://www.rust-lang.org"),
))
.to_srv_request();
let host = Host("www.rust-lang.org").scheme("https");
assert!(host.check(&req.guard_ctx()));
let host = Host("www.rust-lang.org");
assert!(host.check(&req.guard_ctx()));
let host = Host("www.rust-lang.org").scheme("http");
assert!(!host.check(&req.guard_ctx()));
let host = Host("blog.rust-lang.org");
assert!(!host.check(&req.guard_ctx()));
let host = Host("blog.rust-lang.org").scheme("https");
assert!(!host.check(&req.guard_ctx()));
let host = Host("crates.io").scheme("https");
assert!(!host.check(&req.guard_ctx()));
let host = Host("localhost");
assert!(!host.check(&req.guard_ctx()));
}
#[test] #[test]
fn method_guards() { fn method_guards() {
let get_req = TestRequest::get().to_srv_request(); let get_req = TestRequest::get().to_srv_request();

View File

@ -76,7 +76,6 @@ impl ConnectionInfo {
for (name, val) in req for (name, val) in req
.headers .headers
.get_all(&header::FORWARDED) .get_all(&header::FORWARDED)
.into_iter()
.filter_map(|hdr| hdr.to_str().ok()) .filter_map(|hdr| hdr.to_str().ok())
// "for=1.2.3.4, for=5.6.7.8; scheme=https" // "for=1.2.3.4, for=5.6.7.8; scheme=https"
.flat_map(|val| val.split(';')) .flat_map(|val| val.split(';'))

View File

@ -69,6 +69,7 @@
#![deny(rust_2018_idioms, nonstandard_style)] #![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)] #![warn(future_incompatible)]
#![allow(clippy::uninlined_format_args)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]

View File

@ -7,7 +7,7 @@ use std::{
}; };
use futures_core::{future::LocalBoxFuture, ready}; use futures_core::{future::LocalBoxFuture, ready};
use futures_util::future::FutureExt as _; use futures_util::FutureExt as _;
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
use crate::{ use crate::{

View File

@ -50,6 +50,8 @@ type DefaultHandler<B> = Option<Rc<ErrorHandler<B>>>;
/// will pass by unchanged by this middleware. /// will pass by unchanged by this middleware.
/// ///
/// # Examples /// # Examples
/// ## Handler Response
/// Header
/// ``` /// ```
/// use actix_web::http::{header, StatusCode}; /// use actix_web::http::{header, StatusCode};
/// use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers}; /// use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers};
@ -67,6 +69,28 @@ type DefaultHandler<B> = Option<Rc<ErrorHandler<B>>>;
/// .wrap(ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, add_error_header)) /// .wrap(ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, add_error_header))
/// .service(web::resource("/").route(web::get().to(HttpResponse::InternalServerError))); /// .service(web::resource("/").route(web::get().to(HttpResponse::InternalServerError)));
/// ``` /// ```
///
/// Body Content
/// ```
/// use actix_web::http::{header, StatusCode};
/// use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers};
/// use actix_web::{dev, web, App, HttpResponse, Result};
/// fn add_error_body<B>(res: dev::ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
/// // Get the error message and status code
/// let error_message = "An error occurred";
/// // Destructures ServiceResponse into request and response components
/// let (req, res) = res.into_parts();
/// // Create a new response with the modified body
/// let res = res.set_body(error_message).map_into_boxed_body();
/// // Create a new ServiceResponse with the modified response
/// let res = dev::ServiceResponse::new(req, res).map_into_right_body();
/// Ok(ErrorHandlerResponse::Response(res))
///}
///
/// let app = App::new()
/// .wrap(ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, add_error_body))
/// .service(web::resource("/").route(web::get().to(HttpResponse::InternalServerError)));
/// ```
/// ## Registering default handler /// ## Registering default handler
/// ``` /// ```
/// # use actix_web::http::{header, StatusCode}; /// # use actix_web::http::{header, StatusCode};
@ -351,7 +375,7 @@ mod tests {
use actix_service::IntoService; use actix_service::IntoService;
use actix_utils::future::ok; use actix_utils::future::ok;
use bytes::Bytes; use bytes::Bytes;
use futures_util::future::FutureExt as _; use futures_util::FutureExt as _;
use super::*; use super::*;
use crate::{ use crate::{

View File

@ -260,7 +260,7 @@ impl HttpRequest {
Ref::map(self.extensions(), |data| data.get().unwrap()) Ref::map(self.extensions(), |data| data.get().unwrap())
} }
/// App config /// Returns a reference to the application's connection configuration.
#[inline] #[inline]
pub fn app_config(&self) -> &AppConfig { pub fn app_config(&self) -> &AppConfig {
self.app_state().config() self.app_state().config()

View File

@ -13,8 +13,9 @@ use crate::{
body::MessageBody, body::MessageBody,
data::Data, data::Data,
dev::{ensure_leading_slash, AppService, ResourceDef}, dev::{ensure_leading_slash, AppService, ResourceDef},
guard::Guard, guard::{self, Guard},
handler::Handler, handler::Handler,
http::header,
route::{Route, RouteService}, route::{Route, RouteService},
service::{ service::{
BoxedHttpService, BoxedHttpServiceFactory, HttpServiceFactory, ServiceRequest, BoxedHttpService, BoxedHttpServiceFactory, HttpServiceFactory, ServiceRequest,
@ -40,8 +41,11 @@ use crate::{
/// .route(web::get().to(|| HttpResponse::Ok()))); /// .route(web::get().to(|| HttpResponse::Ok())));
/// ``` /// ```
/// ///
/// If no matching route could be found, *405* response code get returned. Default behavior could be /// If no matching route is found, [a 405 response is returned with an appropriate Allow header][RFC
/// overridden with `default_resource()` method. /// 9110 §15.5.6]. This default behavior can be overridden using
/// [`default_service()`](Self::default_service).
///
/// [RFC 9110 §15.5.6]: https://www.rfc-editor.org/rfc/rfc9110.html#section-15.5.6
pub struct Resource<T = ResourceEndpoint> { pub struct Resource<T = ResourceEndpoint> {
endpoint: T, endpoint: T,
rdef: Patterns, rdef: Patterns,
@ -66,7 +70,19 @@ impl Resource {
guards: Vec::new(), guards: Vec::new(),
app_data: None, app_data: None,
default: boxed::factory(fn_service(|req: ServiceRequest| async { default: boxed::factory(fn_service(|req: ServiceRequest| async {
use crate::HttpMessage as _;
let allowed = req.extensions().get::<guard::RegisteredMethods>().cloned();
if let Some(methods) = allowed {
Ok(req.into_response(
HttpResponse::MethodNotAllowed()
.insert_header(header::Allow(methods.0))
.finish(),
))
} else {
Ok(req.into_response(HttpResponse::MethodNotAllowed())) Ok(req.into_response(HttpResponse::MethodNotAllowed()))
}
})), })),
} }
} }
@ -309,13 +325,28 @@ where
} }
} }
/// Default service to be used if no matching route could be found. /// Sets the default service to be used if no matching route is found.
/// ///
/// You can use a [`Route`] as default service. /// Unlike [`Scope`]s, a `Resource` does _not_ inherit its parent's default service. You can
/// use a [`Route`] as default service.
/// ///
/// If a default service is not registered, an empty `405 Method Not Allowed` response will be /// If a custom default service is not registered, an empty `405 Method Not Allowed` response
/// sent to the client instead. Unlike [`Scope`](crate::Scope)s, a [`Resource`] does **not** /// with an appropriate Allow header will be sent instead.
/// inherit its parent's default service. ///
/// # Examples
/// ```
/// use actix_web::{App, HttpResponse, web};
///
/// let resource = web::resource("/test")
/// .route(web::get().to(HttpResponse::Ok))
/// .default_service(web::to(|| {
/// HttpResponse::BadRequest()
/// }));
///
/// App::new().service(resource);
/// ```
///
/// [`Scope`]: crate::Scope
pub fn default_service<F, U>(mut self, f: F) -> Self pub fn default_service<F, U>(mut self, f: F) -> Self
where where
F: IntoServiceFactory<U, ServiceRequest>, F: IntoServiceFactory<U, ServiceRequest>,
@ -606,7 +637,11 @@ mod tests {
async fn test_default_resource() { async fn test_default_resource() {
let srv = init_service( let srv = init_service(
App::new() App::new()
.service(web::resource("/test").route(web::get().to(HttpResponse::Ok))) .service(
web::resource("/test")
.route(web::get().to(HttpResponse::Ok))
.route(web::delete().to(HttpResponse::Ok)),
)
.default_service(|r: ServiceRequest| { .default_service(|r: ServiceRequest| {
ok(r.into_response(HttpResponse::BadRequest())) ok(r.into_response(HttpResponse::BadRequest()))
}), }),
@ -621,6 +656,10 @@ mod tests {
.to_request(); .to_request();
let resp = call_service(&srv, req).await; let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
assert_eq!(
resp.headers().get(header::ALLOW).unwrap().as_bytes(),
b"GET, DELETE"
);
let srv = init_service( let srv = init_service(
App::new().service( App::new().service(

View File

@ -21,7 +21,7 @@ use crate::{Error, HttpRequest, HttpResponse};
/// - `HttpResponse` and `HttpResponseBuilder` /// - `HttpResponse` and `HttpResponseBuilder`
/// - `Option<R>` where `R: Responder` /// - `Option<R>` where `R: Responder`
/// - `Result<R, E>` where `R: Responder` and [`E: ResponseError`](crate::ResponseError) /// - `Result<R, E>` where `R: Responder` and [`E: ResponseError`](crate::ResponseError)
/// - `(R, StatusCode) where `R: Responder` /// - `(R, StatusCode)` where `R: Responder`
/// - `&'static str`, `String`, `&'_ String`, `Cow<'_, str>`, [`ByteString`](bytestring::ByteString) /// - `&'static str`, `String`, `&'_ String`, `Cow<'_, str>`, [`ByteString`](bytestring::ByteString)
/// - `&'static [u8]`, `Vec<u8>`, `Bytes`, `BytesMut` /// - `&'static [u8]`, `Vec<u8>`, `Bytes`, `BytesMut`
/// - [`Json<T>`](crate::web::Json) and [`Form<T>`](crate::web::Form) where `T: Serialize` /// - [`Json<T>`](crate::web::Json) and [`Form<T>`](crate::web::Form) where `T: Serialize`

View File

@ -238,11 +238,7 @@ impl ServiceRequest {
self.req.connection_info() self.req.connection_info()
} }
/// Returns reference to the Path parameters. /// Counterpart to [`HttpRequest::match_info`].
///
/// 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] #[inline]
pub fn match_info(&self) -> &Path<Url> { pub fn match_info(&self) -> &Path<Url> {
self.req.match_info() self.req.match_info()
@ -267,12 +263,13 @@ impl ServiceRequest {
} }
/// Returns a reference to the application's resource map. /// Returns a reference to the application's resource map.
/// Counterpart to [`HttpRequest::resource_map`].
#[inline] #[inline]
pub fn resource_map(&self) -> &ResourceMap { pub fn resource_map(&self) -> &ResourceMap {
self.req.resource_map() self.req.resource_map()
} }
/// Returns a reference to the application's configuration. /// Counterpart to [`HttpRequest::app_config`].
#[inline] #[inline]
pub fn app_config(&self) -> &AppConfig { pub fn app_config(&self) -> &AppConfig {
self.req.app_config() self.req.app_config()

View File

@ -10,12 +10,16 @@
//! # Calling Test Service //! # Calling Test Service
//! - [`TestRequest`] //! - [`TestRequest`]
//! - [`call_service`] //! - [`call_service`]
//! - [`try_call_service`]
//! - [`call_and_read_body`] //! - [`call_and_read_body`]
//! - [`call_and_read_body_json`] //! - [`call_and_read_body_json`]
//! - [`try_call_and_read_body_json`]
//! //!
//! # Reading Response Payloads //! # Reading Response Payloads
//! - [`read_body`] //! - [`read_body`]
//! - [`try_read_body`]
//! - [`read_body_json`] //! - [`read_body_json`]
//! - [`try_read_body_json`]
// TODO: more docs on generally how testing works with these parts // TODO: more docs on generally how testing works with these parts
@ -31,7 +35,8 @@ pub use self::test_services::{default_service, ok_service, simple_service, statu
#[allow(deprecated)] #[allow(deprecated)]
pub use self::test_utils::{ pub use self::test_utils::{
call_and_read_body, call_and_read_body_json, call_service, init_service, read_body, call_and_read_body, call_and_read_body_json, call_service, init_service, read_body,
read_body_json, read_response, read_response_json, read_body_json, read_response, read_response_json, try_call_and_read_body_json,
try_call_service, try_read_body, try_read_body_json,
}; };
#[cfg(test)] #[cfg(test)]

View File

@ -100,6 +100,15 @@ where
.expect("test service call returned error") .expect("test service call returned error")
} }
/// Fallible version of [`call_service`] that allows testing response completion errors.
pub async fn try_call_service<S, R, B, E>(app: &S, req: R) -> Result<S::Response, E>
where
S: Service<R, Response = ServiceResponse<B>, Error = E>,
E: std::fmt::Debug,
{
app.call(req).await
}
/// Helper function that returns a response body of a TestRequest /// Helper function that returns a response body of a TestRequest
/// ///
/// # Examples /// # Examples
@ -185,13 +194,23 @@ pub async fn read_body<B>(res: ServiceResponse<B>) -> Bytes
where where
B: MessageBody, B: MessageBody,
{ {
let body = res.into_body(); try_read_body(res)
body::to_bytes(body)
.await .await
.map_err(Into::<Box<dyn StdError>>::into) .map_err(Into::<Box<dyn StdError>>::into)
.expect("error reading test response body") .expect("error reading test response body")
} }
/// Fallible version of [`read_body`] that allows testing MessageBody reading errors.
pub async fn try_read_body<B>(
res: ServiceResponse<B>,
) -> Result<Bytes, <B as MessageBody>::Error>
where
B: MessageBody,
{
let body = res.into_body();
body::to_bytes(body).await
}
/// Helper function that returns a deserialized response body of a ServiceResponse. /// Helper function that returns a deserialized response body of a ServiceResponse.
/// ///
/// # Examples /// # Examples
@ -240,18 +259,27 @@ where
B: MessageBody, B: MessageBody,
T: DeserializeOwned, T: DeserializeOwned,
{ {
let body = read_body(res).await; try_read_body_json(res).await.unwrap_or_else(|err| {
serde_json::from_slice(&body).unwrap_or_else(|err| {
panic!( panic!(
"could not deserialize body into a {}\nerr: {}\nbody: {:?}", "could not deserialize body into a {}\nerr: {}",
std::any::type_name::<T>(), std::any::type_name::<T>(),
err, err,
body,
) )
}) })
} }
/// Fallible version of [`read_body_json`] that allows testing response deserialization errors.
pub async fn try_read_body_json<T, B>(res: ServiceResponse<B>) -> Result<T, Box<dyn StdError>>
where
B: MessageBody,
T: DeserializeOwned,
{
let body = try_read_body(res)
.await
.map_err(Into::<Box<dyn StdError>>::into)?;
serde_json::from_slice(&body).map_err(Into::<Box<dyn StdError>>::into)
}
/// Helper function that returns a deserialized response body of a TestRequest /// Helper function that returns a deserialized response body of a TestRequest
/// ///
/// # Examples /// # Examples
@ -299,8 +327,23 @@ where
B: MessageBody, B: MessageBody,
T: DeserializeOwned, T: DeserializeOwned,
{ {
let res = call_service(app, req).await; try_call_and_read_body_json(app, req).await.unwrap()
read_body_json(res).await }
/// Fallible version of [`call_and_read_body_json`] that allows testing service call errors.
pub async fn try_call_and_read_body_json<S, B, T>(
app: &S,
req: Request,
) -> Result<T, Box<dyn StdError>>
where
S: Service<Request, Response = ServiceResponse<B>, Error = Error>,
B: MessageBody,
T: DeserializeOwned,
{
let res = try_call_service(app, req)
.await
.map_err(Into::<Box<dyn StdError>>::into)?;
try_read_body_json(res).await
} }
#[doc(hidden)] #[doc(hidden)]
@ -358,7 +401,7 @@ mod tests {
assert_eq!(result, Bytes::from_static(b"delete!")); assert_eq!(result, Bytes::from_static(b"delete!"));
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Debug)]
pub struct Person { pub struct Person {
id: String, id: String,
name: String, name: String,
@ -383,6 +426,26 @@ mod tests {
assert_eq!(&result.id, "12345"); assert_eq!(&result.id, "12345");
} }
#[actix_rt::test]
async fn test_try_response_json_error() {
let app = init_service(App::new().service(web::resource("/people").route(
web::post().to(|person: web::Json<Person>| HttpResponse::Ok().json(person)),
)))
.await;
let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes();
let req = TestRequest::post()
.uri("/animals") // Not registered to ensure an error occurs.
.insert_header((header::CONTENT_TYPE, "application/json"))
.set_payload(payload)
.to_request();
let result: Result<Person, Box<dyn StdError>> =
try_call_and_read_body_json(&app, req).await;
assert!(result.is_err());
}
#[actix_rt::test] #[actix_rt::test]
async fn test_body_json() { async fn test_body_json() {
let app = init_service(App::new().service(web::resource("/people").route( let app = init_service(App::new().service(web::resource("/people").route(
@ -403,6 +466,27 @@ mod tests {
assert_eq!(&result.name, "User name"); assert_eq!(&result.name, "User name");
} }
#[actix_rt::test]
async fn test_try_body_json_error() {
let app = init_service(App::new().service(web::resource("/people").route(
web::post().to(|person: web::Json<Person>| HttpResponse::Ok().json(person)),
)))
.await;
// Use a number for id to cause a deserialization error.
let payload = r#"{"id":12345,"name":"User name"}"#.as_bytes();
let res = TestRequest::post()
.uri("/people")
.insert_header((header::CONTENT_TYPE, "application/json"))
.set_payload(payload)
.send_request(&app)
.await;
let result: Result<Person, Box<dyn StdError>> = try_read_body_json(res).await;
assert!(result.is_err());
}
#[actix_rt::test] #[actix_rt::test]
async fn test_request_response_form() { async fn test_request_response_form() {
let app = init_service(App::new().service(web::resource("/people").route( let app = init_service(App::new().service(web::resource("/people").route(

View File

@ -27,7 +27,7 @@ use crate::{
/// # Examples /// # Examples
/// ``` /// ```
/// use std::future::Future; /// use std::future::Future;
/// use futures_util::stream::StreamExt as _; /// use futures_util::StreamExt as _;
/// use actix_web::{post, web}; /// use actix_web::{post, web};
/// ///
/// // `body: web::Payload` parameter extracts raw payload stream from request /// // `body: web::Payload` parameter extracts raw payload stream from request

View File

@ -177,7 +177,7 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use futures_util::stream::StreamExt as _; use futures_util::StreamExt as _;
use super::*; use super::*;
use crate::test::TestRequest; use crate::test::TestRequest;

View File

@ -1,3 +1,5 @@
#![allow(clippy::uninlined_format_args)]
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
extern crate tls_openssl as openssl; extern crate tls_openssl as openssl;

View File

@ -1,6 +1,11 @@
# Changes # Changes
## Unreleased - 2022-xx-xx ## Unreleased - 2023-xx-xx
### Changed
- `client::Connect` is now public to allow tunneling connection with `client::Connector`.
## 3.1.0 - 2023-01-21
### Changed ### Changed
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency. - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "awc" name = "awc"
version = "3.0.1" version = "3.1.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Async HTTP and WebSocket client library" description = "Async HTTP and WebSocket client library"
keywords = ["actix", "http", "framework", "async", "web"] keywords = ["actix", "http", "framework", "async", "web"]
@ -57,18 +57,18 @@ dangerous-h2c = []
[dependencies] [dependencies]
actix-codec = "0.5" actix-codec = "0.5"
actix-service = "2" actix-service = "2"
actix-http = { version = "3", features = ["http2", "ws"] } actix-http = { version = "3.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" actix-utils = "3"
ahash = "0.7" ahash = "0.7"
base64 = "0.13" base64 = "0.21"
bytes = "1" bytes = "1"
cfg-if = "1" cfg-if = "1"
derive_more = "0.99.5" derive_more = "0.99.5"
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] } futures-util = { version = "0.3.17", default-features = false, features = ["alloc", "sink"] }
h2 = "0.3.9" h2 = "0.3.9"
http = "0.2.5" http = "0.2.5"
itoa = "1" itoa = "1"
@ -80,14 +80,14 @@ rand = "0.8"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
serde_urlencoded = "0.7" serde_urlencoded = "0.7"
tokio = { version = "1.8.4", features = ["sync"] } tokio = { version = "1.18.5", features = ["sync"] }
cookie = { version = "0.16", features = ["percent-encode"], optional = true } cookie = { version = "0.16", features = ["percent-encode"], optional = true }
tls-openssl = { package = "openssl", version = "0.10.9", optional = true } tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features = ["dangerous_configuration"] } tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features = ["dangerous_configuration"] }
trust-dns-resolver = { version = "0.21", optional = true } trust-dns-resolver = { version = "0.22", optional = true }
[dev-dependencies] [dev-dependencies]
actix-http = { version = "3", features = ["openssl"] } actix-http = { version = "3", features = ["openssl"] }
@ -102,12 +102,12 @@ brotli = "3.3.3"
const-str = "0.4" const-str = "0.4"
env_logger = "0.9" env_logger = "0.9"
flate2 = "1.0.13" flate2 = "1.0.13"
futures-util = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.17", default-features = false }
static_assertions = "1.1" static_assertions = "1.1"
rcgen = "0.9" rcgen = "0.9"
rustls-pemfile = "1" rustls-pemfile = "1"
tokio = { version = "1.13.1", features = ["rt-multi-thread", "macros"] } tokio = { version = "1.18.5", features = ["rt-multi-thread", "macros"] }
zstd = "0.11" zstd = "0.12"
[[example]] [[example]]
name = "client" name = "client"

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.1)](https://docs.rs/awc/3.0.1) [![Documentation](https://docs.rs/awc/badge.svg?version=3.1.0)](https://docs.rs/awc/3.1.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.1/status.svg)](https://deps.rs/crate/awc/3.0.1) [![Dependency Status](https://deps.rs/crate/awc/3.1.0/status.svg)](https://deps.rs/crate/awc/3.1.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

@ -1,3 +1,5 @@
#![allow(clippy::uninlined_format_args)]
use std::error::Error as StdError; use std::error::Error as StdError;
#[tokio::main] #[tokio::main]

View File

@ -1,5 +1,7 @@
use std::{convert::TryFrom, fmt, net::IpAddr, rc::Rc, time::Duration}; use std::{convert::TryFrom, fmt, net::IpAddr, rc::Rc, time::Duration};
use base64::prelude::*;
use actix_http::{ use actix_http::{
error::HttpError, error::HttpError,
header::{self, HeaderMap, HeaderName, TryIntoHeaderPair}, header::{self, HeaderMap, HeaderName, TryIntoHeaderPair},
@ -210,7 +212,7 @@ where
}; };
self.add_default_header(( self.add_default_header((
header::AUTHORIZATION, header::AUTHORIZATION,
format!("Basic {}", base64::encode(&auth)), format!("Basic {}", BASE64_STANDARD.encode(auth)),
)) ))
} }

View File

@ -19,7 +19,7 @@ use actix_rt::time::{sleep, Sleep};
use actix_service::Service; use actix_service::Service;
use ahash::AHashMap; use ahash::AHashMap;
use futures_core::future::LocalBoxFuture; use futures_core::future::LocalBoxFuture;
use futures_util::FutureExt; use futures_util::FutureExt as _;
use http::uri::Authority; use http::uri::Authority;
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
use tokio::sync::{OwnedSemaphorePermit, Semaphore}; use tokio::sync::{OwnedSemaphorePermit, Semaphore};

View File

@ -83,7 +83,7 @@
//! ```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 as _, stream::StreamExt as _}; //! use futures_util::{SinkExt as _, 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")
@ -105,7 +105,8 @@
#![allow( #![allow(
clippy::type_complexity, clippy::type_complexity,
clippy::borrow_interior_mutable_const, clippy::borrow_interior_mutable_const,
clippy::needless_doctest_main clippy::needless_doctest_main,
clippy::uninlined_format_args
)] )]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
@ -138,7 +139,7 @@ pub mod http {
} }
pub use self::builder::ClientBuilder; pub use self::builder::ClientBuilder;
pub use self::client::{Client, Connector}; pub use self::client::{Client, Connect, Connector};
pub use self::connect::{BoxConnectorService, BoxedSocket, ConnectRequest, ConnectResponse}; pub use self::connect::{BoxConnectorService, BoxedSocket, ConnectRequest, ConnectResponse};
pub use self::frozen::{FrozenClientRequest, FrozenSendBuilder}; pub use self::frozen::{FrozenClientRequest, FrozenSendBuilder};
pub use self::request::ClientRequest; pub use self::request::ClientRequest;

View File

@ -1,5 +1,6 @@
use std::{convert::TryFrom, fmt, net, rc::Rc, time::Duration}; use std::{convert::TryFrom, fmt, net, rc::Rc, time::Duration};
use base64::prelude::*;
use bytes::Bytes; use bytes::Bytes;
use futures_core::Stream; use futures_core::Stream;
use serde::Serialize; use serde::Serialize;
@ -238,7 +239,7 @@ impl ClientRequest {
self.insert_header(( self.insert_header((
header::AUTHORIZATION, header::AUTHORIZATION,
format!("Basic {}", base64::encode(&auth)), format!("Basic {}", BASE64_STANDARD.encode(auth)),
)) ))
} }
@ -565,6 +566,8 @@ mod tests {
assert_eq!(req.head.version, Version::HTTP_2); assert_eq!(req.head.version, Version::HTTP_2);
let _ = req.headers_mut(); let _ = req.headers_mut();
#[allow(clippy::let_underscore_future)]
let _ = req.send_body(""); let _ = req.send_body("");
} }

View File

@ -6,7 +6,7 @@
//! //!
//! ```no_run //! ```no_run
//! use awc::{Client, ws}; //! use awc::{Client, ws};
//! use futures_util::{sink::SinkExt as _, stream::StreamExt as _}; //! use futures_util::{SinkExt as _, StreamExt as _};
//! //!
//! #[actix_rt::main] //! #[actix_rt::main]
//! async fn main() { //! async fn main() {
@ -28,6 +28,8 @@
use std::{convert::TryFrom, fmt, net::SocketAddr, str}; use std::{convert::TryFrom, fmt, net::SocketAddr, str};
use base64::prelude::*;
use actix_codec::Framed; use actix_codec::Framed;
use actix_http::{ws, Payload, RequestHead}; use actix_http::{ws, Payload, RequestHead};
use actix_rt::time::timeout; use actix_rt::time::timeout;
@ -236,7 +238,10 @@ impl WebsocketsRequest {
Some(password) => format!("{}:{}", username, password), Some(password) => format!("{}:{}", username, password),
None => format!("{}:", username), None => format!("{}:", username),
}; };
self.header(AUTHORIZATION, format!("Basic {}", base64::encode(&auth))) self.header(
AUTHORIZATION,
format!("Basic {}", BASE64_STANDARD.encode(auth)),
)
} }
/// Set HTTP bearer authentication header /// Set HTTP bearer authentication header
@ -321,7 +326,7 @@ impl WebsocketsRequest {
// Generate a random key for the `Sec-WebSocket-Key` header which is a base64-encoded // Generate a random key for the `Sec-WebSocket-Key` header which is a base64-encoded
// (see RFC 4648 §4) value that, when decoded, is 16 bytes in length (RFC 6455 §1.3). // (see RFC 4648 §4) value that, when decoded, is 16 bytes in length (RFC 6455 §1.3).
let sec_key: [u8; 16] = rand::random(); let sec_key: [u8; 16] = rand::random();
let key = base64::encode(sec_key); let key = BASE64_STANDARD.encode(sec_key);
self.head.headers.insert( self.head.headers.insert(
header::SEC_WEBSOCKET_KEY, header::SEC_WEBSOCKET_KEY,
@ -503,6 +508,8 @@ mod tests {
.unwrap(), .unwrap(),
"Bearer someS3cr3tAutht0k3n" "Bearer someS3cr3tAutht0k3n"
); );
#[allow(clippy::let_underscore_future)]
let _ = req.connect(); let _ = req.connect();
} }

View File

@ -1,3 +1,5 @@
#![allow(clippy::uninlined_format_args)]
use std::{ use std::{
collections::HashMap, collections::HashMap,
convert::Infallible, convert::Infallible,
@ -11,6 +13,7 @@ use std::{
}; };
use actix_utils::future::ok; use actix_utils::future::ok;
use base64::prelude::*;
use bytes::Bytes; use bytes::Bytes;
use cookie::Cookie; use cookie::Cookie;
use futures_util::stream; use futures_util::stream;
@ -139,7 +142,7 @@ async fn timeout_override() {
#[actix_rt::test] #[actix_rt::test]
async fn response_timeout() { async fn response_timeout() {
use futures_util::stream::{once, StreamExt as _}; use futures_util::{stream::once, StreamExt as _};
let srv = actix_test::start(|| { let srv = actix_test::start(|| {
App::new().service(web::resource("/").route(web::to(|| async { App::new().service(web::resource("/").route(web::to(|| async {
@ -781,7 +784,7 @@ async fn client_basic_auth() {
.unwrap() .unwrap()
.to_str() .to_str()
.unwrap() .unwrap()
== format!("Basic {}", base64::encode("username:password")) == format!("Basic {}", BASE64_STANDARD.encode("username:password"))
{ {
HttpResponse::Ok() HttpResponse::Ok()
} else { } else {