mirror of https://github.com/fafhrd91/actix-web
Merge branch 'master' into multipart-forms
This commit is contained in:
commit
8578dadbb2
|
@ -1,8 +1,12 @@
|
|||
# Changes
|
||||
|
||||
## Unreleased - 2022-xx-xx
|
||||
|
||||
|
||||
## 0.6.3 - 2023-01-21
|
||||
- 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.
|
||||
- Update `tokio-uring` dependency to `0.4`.
|
||||
|
||||
[#2903]: https://github.com/actix/actix-web/pull/2903
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "actix-files"
|
||||
version = "0.6.2"
|
||||
version = "0.6.3"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Rob Ede <robjtede@icloud.com>",
|
||||
|
@ -29,7 +29,7 @@ actix-web = { version = "4", default-features = false }
|
|||
bitflags = "1"
|
||||
bytes = "1"
|
||||
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"
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
|
@ -40,8 +40,8 @@ v_htmlescape= "0.15"
|
|||
|
||||
# experimental-io-uring
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
tokio-uring = { version = "0.3", optional = true, features = ["bytes"] }
|
||||
actix-server = { version = "2.1", optional = true } # ensure matching tokio-uring versions
|
||||
tokio-uring = { version = "0.4", optional = true, features = ["bytes"] }
|
||||
actix-server = { version = "2.2", optional = true } # ensure matching tokio-uring versions
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "2.7"
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
> Static file serving for Actix Web
|
||||
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://docs.rs/actix-files/0.6.2)
|
||||
[](https://docs.rs/actix-files/0.6.3)
|
||||

|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-files/0.6.2)
|
||||
[](https://deps.rs/crate/actix-files/0.6.3)
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
|
|
@ -142,7 +142,7 @@ impl Files {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set custom directory renderer
|
||||
/// Set custom directory renderer.
|
||||
pub fn files_listing_renderer<F>(mut self, f: F) -> Self
|
||||
where
|
||||
for<'r, 's> F:
|
||||
|
@ -152,7 +152,7 @@ impl Files {
|
|||
self
|
||||
}
|
||||
|
||||
/// Specifies mime override callback
|
||||
/// Specifies MIME override callback.
|
||||
pub fn mime_override<F>(mut self, f: F) -> Self
|
||||
where
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||
#![warn(future_incompatible, missing_docs, missing_debug_implementations)]
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
use actix_service::boxed::{BoxService, BoxServiceFactory};
|
||||
use actix_web::{
|
||||
|
|
|
@ -30,7 +30,7 @@ impl PathBufWrap {
|
|||
let mut segment_count = path.matches('/').count() + 1;
|
||||
|
||||
// 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)
|
||||
.decode_utf8()
|
||||
.map_err(|_| UriSegmentError::NotValidUtf8)?;
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
# Changes
|
||||
|
||||
## Unreleased - 2022-xx-xx
|
||||
|
||||
|
||||
## 3.1.0 - 2023-01-21
|
||||
- Minimum supported Rust version (MSRV) is now 1.59.
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "actix-http-test"
|
||||
version = "3.0.0"
|
||||
version = "3.1.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Various helpers for Actix applications to use during testing"
|
||||
keywords = ["http", "web", "framework", "async", "futures"]
|
||||
|
@ -37,9 +37,8 @@ actix-rt = "2.2"
|
|||
actix-server = "2"
|
||||
awc = { version = "3", default-features = false }
|
||||
|
||||
base64 = "0.13"
|
||||
bytes = "1"
|
||||
futures-core = { version = "0.3.7", default-features = false }
|
||||
futures-core = { version = "0.3.17", default-features = false }
|
||||
http = "0.2.5"
|
||||
log = "0.4"
|
||||
socket2 = "0.4"
|
||||
|
@ -48,7 +47,7 @@ serde_json = "1.0"
|
|||
slab = "0.4"
|
||||
serde_urlencoded = "0.7"
|
||||
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]
|
||||
actix-web = { version = "4", default-features = false, features = ["cookies"] }
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
> Various helpers for Actix applications to use during testing.
|
||||
|
||||
[](https://crates.io/crates/actix-http-test)
|
||||
[](https://docs.rs/actix-http-test/3.0.0)
|
||||
[](https://docs.rs/actix-http-test/3.1.0)
|
||||

|
||||

|
||||
<br>
|
||||
[](https://deps.rs/crate/actix-http-test/3.0.0)
|
||||
[](https://deps.rs/crate/actix-http-test/3.1.0)
|
||||
[](https://crates.io/crates/actix-http-test)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||
#![warn(future_incompatible)]
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![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
|
||||
// all thread managed resources should be dropped at this point
|
||||
#[allow(clippy::let_underscore_future)]
|
||||
let _ = thread_stop_tx.send(());
|
||||
});
|
||||
|
||||
|
@ -294,6 +296,7 @@ impl Drop for TestServer {
|
|||
// without needing to await anything
|
||||
|
||||
// signal server to stop
|
||||
#[allow(clippy::let_underscore_future)]
|
||||
let _ = self.server.stop(true);
|
||||
|
||||
// signal system to stop
|
||||
|
|
|
@ -1,15 +1,39 @@
|
|||
# Changes
|
||||
|
||||
## Unreleased - 2022-xx-xx
|
||||
|
||||
|
||||
## 3.3.0 - 2023-01-21
|
||||
### 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 `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
|
||||
- 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
|
||||
[#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
|
||||
|
@ -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]
|
||||
- `ResponseBuilder::message_body` now returns a `Result`. [#2201]
|
||||
- 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
|
||||
- Stop re-exporting `http` crate's `HeaderMap` types in addition to ours. [#2171]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "actix-http"
|
||||
version = "3.2.2"
|
||||
version = "3.3.0"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Rob Ede <robjtede@icloud.com>",
|
||||
|
@ -67,7 +67,7 @@ bytes = "1"
|
|||
bytestring = "1"
|
||||
derive_more = "0.99.5"
|
||||
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"
|
||||
httparse = "1.5.1"
|
||||
httpdate = "1.0.1"
|
||||
|
@ -77,7 +77,7 @@ mime = "0.3"
|
|||
percent-encoding = "2.1"
|
||||
pin-project-lite = "0.2"
|
||||
smallvec = "1.6.1"
|
||||
tokio = { version = "1.13.1", features = [] }
|
||||
tokio = { version = "1.18.5", features = [] }
|
||||
tokio-util = { version = "0.7", features = ["io", "codec"] }
|
||||
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
||||
|
||||
|
@ -86,7 +86,7 @@ h2 = { version = "0.3.9", optional = true }
|
|||
|
||||
# websockets
|
||||
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 }
|
||||
sha1 = { version = "0.10", optional = true }
|
||||
|
||||
|
@ -96,7 +96,7 @@ actix-tls = { version = "3", default-features = false, optional = true }
|
|||
# compress-*
|
||||
brotli = { version = "3.3.3", optional = true }
|
||||
flate2 = { version = "1.0.13", optional = true }
|
||||
zstd = { version = "0.11", optional = true }
|
||||
zstd = { version = "0.12", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-http-test = { version = "3", features = ["openssl"] }
|
||||
|
@ -105,9 +105,9 @@ actix-tls = { version = "3", features = ["openssl"] }
|
|||
actix-web = "4"
|
||||
|
||||
async-stream = "0.3"
|
||||
criterion = { version = "0.3", features = ["html_reports"] }
|
||||
criterion = { version = "0.4", features = ["html_reports"] }
|
||||
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"
|
||||
once_cell = "1.9"
|
||||
rcgen = "0.9"
|
||||
|
@ -119,7 +119,7 @@ serde_json = "1.0"
|
|||
static_assertions = "1"
|
||||
tls-openssl = { package = "openssl", version = "0.10.9" }
|
||||
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]]
|
||||
name = "ws"
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
> HTTP primitives for the Actix ecosystem.
|
||||
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://docs.rs/actix-http/3.2.2)
|
||||
[](https://docs.rs/actix-http/3.3.0)
|
||||

|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-http/3.2.2)
|
||||
[](https://deps.rs/crate/actix-http/3.3.0)
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
|
||||
const CODES: &[u16] = &[0, 1000, 201, 800, 550];
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -120,7 +120,7 @@ pub trait MessageBody {
|
|||
}
|
||||
|
||||
mod foreign_impls {
|
||||
use std::ops::DerefMut;
|
||||
use std::{borrow::Cow, ops::DerefMut};
|
||||
|
||||
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 {
|
||||
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 {
|
||||
type Error = Infallible;
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ where
|
|||
|
||||
#[inline]
|
||||
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`].
|
||||
|
|
|
@ -186,7 +186,7 @@ where
|
|||
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>
|
||||
where
|
||||
B: MessageBody,
|
||||
|
@ -209,7 +209,7 @@ where
|
|||
.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_attr(docsrs, doc(cfg(feature = "http2")))]
|
||||
pub fn h2<F, B>(self, service: F) -> crate::h2::H2Service<T, S, B>
|
||||
|
|
|
@ -71,7 +71,7 @@ impl ChunkedState {
|
|||
|
||||
match size.checked_mul(radix) {
|
||||
Some(n) => {
|
||||
*size = n as u64;
|
||||
*size = n;
|
||||
*size += rem as u64;
|
||||
|
||||
Poll::Ready(Ok(ChunkedState::Size))
|
||||
|
|
|
@ -64,7 +64,7 @@ fn drop_payload_service(
|
|||
fn echo_payload_service() -> impl Service<Request, Response = Response<Bytes>, Error = Error> {
|
||||
fn_service(|mut req: Request| {
|
||||
Box::pin(async move {
|
||||
use futures_util::stream::StreamExt as _;
|
||||
use futures_util::StreamExt as _;
|
||||
|
||||
let mut pl = req.take_payload();
|
||||
let mut body = BytesMut::new();
|
||||
|
|
|
@ -450,7 +450,7 @@ impl TransferEncoding {
|
|||
|
||||
buf.extend_from_slice(&msg[..len as usize]);
|
||||
|
||||
*remaining -= len as u64;
|
||||
*remaining -= len;
|
||||
Ok(*remaining == 0)
|
||||
} else {
|
||||
Ok(true)
|
||||
|
|
|
@ -29,7 +29,7 @@ use crate::{
|
|||
HeaderName, HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, UPGRADE,
|
||||
},
|
||||
service::HttpFlow,
|
||||
Extensions, OnConnectData, Payload, Request, Response, ResponseHead,
|
||||
Extensions, Method, OnConnectData, Payload, Request, Response, ResponseHead,
|
||||
};
|
||||
|
||||
const CHUNK_SIZE: usize = 16_384;
|
||||
|
@ -118,6 +118,7 @@ where
|
|||
let payload = crate::h2::Payload::new(body);
|
||||
let pl = Payload::H2 { payload };
|
||||
let mut req = Request::with_payload(pl);
|
||||
let head_req = parts.method == Method::HEAD;
|
||||
|
||||
let head = req.head_mut();
|
||||
head.uri = parts.uri;
|
||||
|
@ -135,10 +136,10 @@ where
|
|||
actix_rt::spawn(async move {
|
||||
// resolve service call and send response.
|
||||
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) => {
|
||||
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>,
|
||||
mut tx: SendResponse<Bytes>,
|
||||
config: ServiceConfig,
|
||||
head_req: bool,
|
||||
) -> Result<(), DispatchError>
|
||||
where
|
||||
B: MessageBody,
|
||||
|
@ -215,14 +217,14 @@ where
|
|||
// prepare response.
|
||||
let mut size = body.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.
|
||||
let mut stream = tx
|
||||
.send_response(res, eof)
|
||||
.send_response(res, eof_or_head)
|
||||
.map_err(DispatchError::SendResponse)?;
|
||||
|
||||
if eof {
|
||||
if eof_or_head {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
|
@ -150,9 +150,7 @@ impl HeaderMap {
|
|||
/// assert_eq!(map.len(), 3);
|
||||
/// ```
|
||||
pub fn len(&self) -> usize {
|
||||
self.inner
|
||||
.iter()
|
||||
.fold(0, |acc, (_, values)| acc + values.len())
|
||||
self.inner.values().map(|vals| vals.len()).sum()
|
||||
}
|
||||
|
||||
/// Returns the number of _keys_ stored in the map.
|
||||
|
@ -552,6 +550,39 @@ impl HeaderMap {
|
|||
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.
|
||||
///
|
||||
/// 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());
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn entries_into_iter() {
|
||||
let mut map = HeaderMap::new();
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
//! 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};
|
||||
|
||||
// re-export from http except header map related items
|
||||
pub use http::header::{
|
||||
pub use ::http::header::{
|
||||
HeaderName, HeaderValue, InvalidHeaderName, InvalidHeaderValue, ToStrError,
|
||||
};
|
||||
|
||||
// re-export const header names
|
||||
pub use http::header::{
|
||||
// re-export const header names, list is explicit so that any updates to `common` module do not
|
||||
// conflict with this set
|
||||
pub use ::http::header::{
|
||||
ACCEPT, ACCEPT_CHARSET, ACCEPT_ENCODING, ACCEPT_LANGUAGE, ACCEPT_RANGES,
|
||||
ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_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};
|
||||
|
||||
mod as_name;
|
||||
mod common;
|
||||
mod into_pair;
|
||||
mod into_value;
|
||||
pub mod map;
|
||||
mod shared;
|
||||
mod utils;
|
||||
|
||||
pub use self::as_name::AsHeaderName;
|
||||
pub use self::into_pair::TryIntoHeaderPair;
|
||||
pub use self::into_value::TryIntoHeaderValue;
|
||||
pub use self::map::HeaderMap;
|
||||
pub use self::shared::{
|
||||
parse_extended_value, q, Charset, ContentEncoding, ExtendedValue, HttpDate, LanguageTag,
|
||||
Quality, QualityItem,
|
||||
pub use self::{
|
||||
as_name::AsHeaderName,
|
||||
into_pair::TryIntoHeaderPair,
|
||||
into_value::TryIntoHeaderValue,
|
||||
map::HeaderMap,
|
||||
shared::{
|
||||
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.
|
||||
|
|
|
@ -21,7 +21,8 @@
|
|||
#![allow(
|
||||
clippy::type_complexity,
|
||||
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_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
|
|
|
@ -24,7 +24,39 @@ use crate::{
|
|||
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> {
|
||||
srv: S,
|
||||
cfg: ServiceConfig,
|
||||
|
@ -163,7 +195,9 @@ where
|
|||
U::Error: fmt::Display + Into<Response<BoxBody>>,
|
||||
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(
|
||||
self,
|
||||
) -> impl ServiceFactory<
|
||||
|
@ -179,6 +213,42 @@ where
|
|||
})
|
||||
.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.
|
||||
|
|
|
@ -3,6 +3,7 @@ use std::{
|
|||
fmt,
|
||||
};
|
||||
|
||||
use base64::prelude::*;
|
||||
use tracing::error;
|
||||
|
||||
/// 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 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);
|
||||
|
||||
hash_b64
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#![cfg(feature = "openssl")]
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
extern crate tls_openssl as openssl;
|
||||
|
||||
|
@ -16,7 +17,7 @@ use actix_utils::future::{err, ok, ready};
|
|||
use bytes::{Bytes, BytesMut};
|
||||
use derive_more::{Display, Error};
|
||||
use futures_core::Stream;
|
||||
use futures_util::stream::{once, StreamExt as _};
|
||||
use futures_util::{stream::once, StreamExt as _};
|
||||
use openssl::{
|
||||
pkey::PKey,
|
||||
ssl::{SslAcceptor, SslMethod},
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#![cfg(feature = "rustls")]
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
extern crate tls_rustls as rustls;
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
io::{Read, Write},
|
||||
|
@ -7,18 +9,15 @@ use std::{
|
|||
|
||||
use actix_http::{
|
||||
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_rt::time::sleep;
|
||||
use actix_rt::{net::TcpStream, time::sleep};
|
||||
use actix_service::fn_service;
|
||||
use actix_utils::future::{err, ok, ready};
|
||||
use bytes::Bytes;
|
||||
use derive_more::{Display, Error};
|
||||
use futures_util::{
|
||||
stream::{once, StreamExt as _},
|
||||
FutureExt as _,
|
||||
};
|
||||
use futures_util::{stream::once, FutureExt as _, StreamExt as _};
|
||||
use regex::Regex;
|
||||
|
||||
#[actix_rt::test]
|
||||
|
@ -858,3 +857,44 @@ async fn not_modified_spec_h1() {
|
|||
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
use std::{
|
||||
cell::Cell,
|
||||
convert::Infallible,
|
||||
|
|
|
@ -2,13 +2,18 @@
|
|||
|
||||
## Unreleased - 2022-xx-xx
|
||||
- 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
|
||||
[#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
|
||||
- No significant changes since `0.4.0-beta.13`.
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "actix-multipart"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Multipart form support for Actix Web"
|
||||
keywords = ["http", "web", "framework", "async", "futures"]
|
||||
|
@ -29,8 +29,7 @@ actix-web = { version = "4", default-features = false }
|
|||
|
||||
bytes = "1"
|
||||
derive_more = "0.99.5"
|
||||
futures-core = "0.3.17"
|
||||
futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
|
||||
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
||||
httparse = "1.3"
|
||||
local-waker = "0.1"
|
||||
log = "0.4"
|
||||
|
@ -49,5 +48,6 @@ actix-multipart-rfc7578 = "0.10"
|
|||
actix-rt = "2.2"
|
||||
actix-test = "0.1.0"
|
||||
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"
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
> Multipart form support for Actix Web.
|
||||
|
||||
[](https://crates.io/crates/actix-multipart)
|
||||
[](https://docs.rs/actix-multipart/0.4.0)
|
||||
[](https://docs.rs/actix-multipart/0.5.0)
|
||||

|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-multipart/0.4.0)
|
||||
[](https://deps.rs/crate/actix-multipart/0.5.0)
|
||||
[](https://crates.io/crates/actix-multipart)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ use crate::server::Multipart;
|
|||
/// ```
|
||||
/// use actix_web::{web, HttpResponse, Error};
|
||||
/// use actix_multipart::Multipart;
|
||||
/// use futures_util::stream::StreamExt as _;
|
||||
/// use futures_util::StreamExt as _;
|
||||
///
|
||||
/// async fn index(mut payload: Multipart) -> Result<HttpResponse, Error> {
|
||||
/// // iterate over multipart stream
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||
#![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))]
|
||||
|
||||
// This allows us to use the actix_multipart_derive within this crate's tests
|
||||
|
|
|
@ -872,7 +872,7 @@ mod tests {
|
|||
FromRequest,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use futures_util::{future::lazy, StreamExt};
|
||||
use futures_util::{future::lazy, StreamExt as _};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ serde = "1"
|
|||
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.3", features = ["html_reports"] }
|
||||
criterion = { version = "0.4", features = ["html_reports"] }
|
||||
http = "0.2.5"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
percent-encoding = "2.1"
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||
#![warn(future_incompatible)]
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
|
||||
|
|
|
@ -37,12 +37,12 @@ actix-utils = "3"
|
|||
actix-web = { version = "4", default-features = false, features = ["cookies"] }
|
||||
awc = { version = "3", default-features = false, features = ["cookies"] }
|
||||
|
||||
futures-core = { version = "0.3.7", default-features = false, features = ["std"] }
|
||||
futures-util = { version = "0.3.7", default-features = false, features = [] }
|
||||
futures-core = { version = "0.3.17", default-features = false, features = ["std"] }
|
||||
futures-util = { version = "0.3.17", default-features = false, features = [] }
|
||||
log = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_urlencoded = "0.7"
|
||||
tls-openssl = { package = "openssl", version = "0.10.9", 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"] }
|
||||
|
|
|
@ -321,6 +321,7 @@ where
|
|||
// all thread managed resources should be dropped at this point
|
||||
});
|
||||
|
||||
#[allow(clippy::let_underscore_future)]
|
||||
let _ = thread_stop_tx.send(());
|
||||
});
|
||||
|
||||
|
@ -567,6 +568,7 @@ impl Drop for TestServer {
|
|||
// without needing to await anything
|
||||
|
||||
// signal server to stop
|
||||
#[allow(clippy::let_underscore_future)]
|
||||
let _ = self.server.stop(true);
|
||||
|
||||
// signal system to stop
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
# Changes
|
||||
|
||||
## Unreleased - 2022-xx-xx
|
||||
|
||||
|
||||
## 4.2.0 - 2023-01-21
|
||||
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "actix-web-actors"
|
||||
version = "4.1.0"
|
||||
version = "4.2.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Actix actors support for Actix Web"
|
||||
keywords = ["actix", "http", "web", "framework", "async"]
|
||||
|
@ -21,9 +21,9 @@ actix-web = { version = "4", default-features = false }
|
|||
|
||||
bytes = "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"
|
||||
tokio = { version = "1.13.1", features = ["sync"] }
|
||||
tokio = { version = "1.18.5", features = ["sync"] }
|
||||
tokio-util = { version = "0.7", features = ["codec"] }
|
||||
|
||||
[dev-dependencies]
|
||||
|
@ -35,4 +35,4 @@ actix-web = { version = "4", features = ["macros"] }
|
|||
mime = "0.3"
|
||||
|
||||
env_logger = "0.9"
|
||||
futures-util = { version = "0.3.7", default-features = false }
|
||||
futures-util = { version = "0.3.17", default-features = false }
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
> Actix actors support for Actix Web.
|
||||
|
||||
[](https://crates.io/crates/actix-web-actors)
|
||||
[](https://docs.rs/actix-web-actors/4.1.0)
|
||||
[](https://docs.rs/actix-web-actors/4.2.0)
|
||||

|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-web-actors/4.1.0)
|
||||
[](https://deps.rs/crate/actix-web-actors/4.2.0)
|
||||
[](https://crates.io/crates/actix-web-actors)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
|
|
@ -57,6 +57,7 @@
|
|||
|
||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||
#![warn(future_incompatible)]
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
mod context;
|
||||
pub mod ws;
|
||||
|
|
|
@ -3,7 +3,7 @@ use actix_http::ws::Codec;
|
|||
use actix_web::{web, App, HttpRequest};
|
||||
use actix_web_actors::ws;
|
||||
use bytes::Bytes;
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use futures_util::{SinkExt as _, StreamExt as _};
|
||||
|
||||
struct Ws;
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
# Changes
|
||||
|
||||
## 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
|
||||
|
|
|
@ -27,6 +27,6 @@ actix-test = "0.1"
|
|||
actix-utils = "3"
|
||||
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"
|
||||
rustversion = "1"
|
||||
|
|
|
@ -105,7 +105,7 @@ mod route;
|
|||
/// ```
|
||||
/// # use actix_web::HttpResponse;
|
||||
/// # use actix_web_codegen::route;
|
||||
/// #[route("/test", method = "GET", method = "HEAD")]
|
||||
/// #[route("/test", method = "GET", method = "HEAD", method = "CUSTOM")]
|
||||
/// async fn example() -> HttpResponse {
|
||||
/// HttpResponse::Ok().finish()
|
||||
/// }
|
||||
|
|
|
@ -6,11 +6,11 @@ use proc_macro2::{Span, TokenStream as TokenStream2};
|
|||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
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,)+
|
||||
) => {
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum MethodType {
|
||||
$(
|
||||
$variant,
|
||||
|
@ -27,7 +27,7 @@ macro_rules! method_type {
|
|||
fn parse(method: &str) -> Result<Self, String> {
|
||||
match method {
|
||||
$(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,
|
||||
Post, POST, post,
|
||||
Put, PUT, put,
|
||||
|
@ -53,13 +53,6 @@ method_type! {
|
|||
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 {
|
||||
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 {
|
||||
path: syn::LitStr,
|
||||
resource_name: Option<syn::LitStr>,
|
||||
guards: Vec<Path>,
|
||||
wrappers: Vec<syn::Type>,
|
||||
methods: HashSet<MethodType>,
|
||||
methods: HashSet<MethodTypeExt>,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
|
@ -99,7 +203,7 @@ impl Args {
|
|||
|
||||
let is_route_macro = method.is_none();
|
||||
if let Some(method) = method {
|
||||
methods.insert(method);
|
||||
methods.insert(MethodTypeExt::Standard(method));
|
||||
}
|
||||
|
||||
for arg in args {
|
||||
|
@ -116,6 +220,7 @@ impl Args {
|
|||
));
|
||||
}
|
||||
},
|
||||
|
||||
NestedMeta::Meta(syn::Meta::NameValue(nv)) => {
|
||||
if nv.path.is_ident("name") {
|
||||
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",
|
||||
));
|
||||
} else if let syn::Lit::Str(ref lit) = nv.lit {
|
||||
let method = MethodType::try_from(lit)?;
|
||||
if !methods.insert(method) {
|
||||
if !methods.insert(MethodTypeExt::try_from(lit)?) {
|
||||
return Err(syn::Error::new_spanned(
|
||||
&nv.lit,
|
||||
&format!(
|
||||
format!(
|
||||
"HTTP method defined more than once: `{}`",
|
||||
lit.value()
|
||||
),
|
||||
|
@ -174,11 +278,13 @@ impl Args {
|
|||
));
|
||||
}
|
||||
}
|
||||
|
||||
arg => {
|
||||
return Err(syn::Error::new_spanned(arg, "Unknown attribute."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Args {
|
||||
path: path.unwrap(),
|
||||
resource_name,
|
||||
|
@ -299,22 +405,19 @@ impl ToTokens for Route {
|
|||
.map_or_else(|| name.to_string(), LitStr::value);
|
||||
|
||||
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();
|
||||
|
||||
if methods.len() > 1 {
|
||||
quote! {
|
||||
.guard(
|
||||
::actix_web::guard::Any(::actix_web::guard::#first())
|
||||
#(.or(::actix_web::guard::#others()))*
|
||||
)
|
||||
}
|
||||
let other_method_guards = others
|
||||
.map(|method_ext| method_ext.to_tokens_multi_guard_or_chain())
|
||||
.collect();
|
||||
|
||||
first.to_tokens_multi_guard(other_method_guards)
|
||||
} else {
|
||||
quote! {
|
||||
.guard(::actix_web::guard::#first())
|
||||
}
|
||||
first.to_tokens_single_guard()
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -325,7 +428,6 @@ impl ToTokens for Route {
|
|||
#(.guard(::actix_web::guard::fn_guard(#guards)))*
|
||||
#(.wrap(#wrappers))*
|
||||
.to(#name);
|
||||
|
||||
::actix_web::dev::HttpServiceFactory::register(__resource, __config);
|
||||
}
|
||||
})
|
||||
|
|
|
@ -86,7 +86,18 @@ async fn get_param_test(_: web::Path<String>) -> impl Responder {
|
|||
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 {
|
||||
HttpResponse::Ok()
|
||||
}
|
||||
|
|
|
@ -9,9 +9,11 @@ fn compile_macros() {
|
|||
t.pass("tests/trybuild/route-ok.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-unexpected-method-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.compile_fail("tests/trybuild/routes-missing-method-fail.rs");
|
||||
t.compile_fail("tests/trybuild/routes-missing-args-fail.rs");
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use actix_web_codegen::*;
|
||||
use actix_web::http::Method;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[route("/", method="UNEXPECTED")]
|
||||
#[route("/", method = "hello")]
|
||||
async fn index() -> String {
|
||||
"Hello World!".to_owned()
|
||||
}
|
||||
|
@ -11,7 +13,7 @@ async fn main() {
|
|||
|
||||
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();
|
||||
assert!(response.status().is_success());
|
||||
}
|
|
@ -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`
|
|
@ -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());
|
||||
}
|
|
@ -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`
|
|
@ -1,18 +1,26 @@
|
|||
# Changelog
|
||||
|
||||
## Unreleased - 2022-xx-xx
|
||||
|
||||
## 4.3.0 - 2023-01-21
|
||||
### 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 `Logger::custom_response_replace()`. [#2631]
|
||||
- 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
|
||||
[#2265]: https://github.com/actix/actix-web/pull/2265
|
||||
[#2631]: https://github.com/actix/actix-web/pull/2631
|
||||
[#2784]: https://github.com/actix/actix-web/pull/2784
|
||||
[#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
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "actix-web"
|
||||
version = "4.2.1"
|
||||
version = "4.3.0"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Rob Ede <robjtede@icloud.com>",
|
||||
|
@ -38,10 +38,7 @@ compress-gzip = ["actix-http/compress-gzip", "__compress"]
|
|||
compress-zstd = ["actix-http/compress-zstd", "__compress"]
|
||||
|
||||
# Routing and runtime proc macros
|
||||
macros = [
|
||||
"actix-macros",
|
||||
"actix-web-codegen",
|
||||
]
|
||||
macros = ["actix-macros", "actix-web-codegen"]
|
||||
|
||||
# Cookies support
|
||||
cookies = ["cookie"]
|
||||
|
@ -71,7 +68,7 @@ actix-service = "2"
|
|||
actix-utils = "3"
|
||||
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-web-codegen = { version = "4.1", optional = true }
|
||||
|
||||
|
@ -82,8 +79,8 @@ cfg-if = "1"
|
|||
cookie = { version = "0.16", features = ["percent-encode"], optional = true }
|
||||
derive_more = "0.99.8"
|
||||
encoding_rs = "0.8"
|
||||
futures-core = { version = "0.3.7", default-features = false }
|
||||
futures-util = { version = "0.3.7", default-features = false }
|
||||
futures-core = { version = "0.3.17", default-features = false }
|
||||
futures-util = { version = "0.3.17", default-features = false }
|
||||
http = "0.2.8"
|
||||
itoa = "1"
|
||||
language-tags = "0.3"
|
||||
|
@ -107,10 +104,10 @@ awc = { version = "3", features = ["openssl"] }
|
|||
|
||||
brotli = "3.3.3"
|
||||
const-str = "0.4"
|
||||
criterion = { version = "0.3", features = ["html_reports"] }
|
||||
criterion = { version = "0.4", features = ["html_reports"] }
|
||||
env_logger = "0.9"
|
||||
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"
|
||||
rcgen = "0.9"
|
||||
rustls-pemfile = "1"
|
||||
|
@ -118,8 +115,8 @@ serde = { version = "1.0", features = ["derive"] }
|
|||
static_assertions = "1"
|
||||
tls-openssl = { package = "openssl", version = "0.10.9" }
|
||||
tls-rustls = { package = "rustls", version = "0.20.0" }
|
||||
tokio = { version = "1.13.1", features = ["rt-multi-thread", "macros"] }
|
||||
zstd = "0.11"
|
||||
tokio = { version = "1.18.5", features = ["rt-multi-thread", "macros"] }
|
||||
zstd = "0.12"
|
||||
|
||||
[[test]]
|
||||
name = "test_server"
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
- `actix_http_test::TestServer` moved to `actix_web::test` module. To start
|
||||
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.
|
||||
|
||||
- Feature `rust-tls` renamed to `rustls`
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
<p>
|
||||
|
||||
[](https://crates.io/crates/actix-web)
|
||||
[](https://docs.rs/actix-web/4.2.1)
|
||||
[](https://docs.rs/actix-web/4.3.0)
|
||||

|
||||

|
||||
[](https://deps.rs/crate/actix-web/4.2.1)
|
||||
[](https://deps.rs/crate/actix-web/4.3.0)
|
||||
<br />
|
||||
[](https://github.com/actix/actix-web/actions/workflows/ci.yml)
|
||||
[](https://codecov.io/gh/actix/actix-web)
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
use actix_web::{web, App, HttpResponse};
|
||||
use awc::Client;
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
use actix_web::{get, middleware, web, App, HttpRequest, HttpResponse, HttpServer};
|
||||
|
||||
#[get("/resource1/{name}/index.html")]
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
use actix_web::{middleware, rt, web, App, HttpRequest, HttpServer};
|
||||
|
||||
async fn index(req: HttpRequest) -> &'static str {
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
//! For an example of extracting a client TLS certificate, see:
|
||||
//! <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 actix_web::{
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
use actix_web::{get, web, HttpRequest};
|
||||
#[cfg(unix)]
|
||||
use actix_web::{middleware, App, Error, HttpResponse, HttpServer};
|
||||
|
|
|
@ -5,7 +5,7 @@ use actix_service::{
|
|||
apply, apply_fn_factory, boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt,
|
||||
Transform,
|
||||
};
|
||||
use futures_util::future::FutureExt as _;
|
||||
use futures_util::FutureExt as _;
|
||||
|
||||
use crate::{
|
||||
app_service::{AppEntry, AppInit, AppRoutingFactory},
|
||||
|
@ -712,6 +712,7 @@ mod tests {
|
|||
.route("/", web::to(|| async { "hello" }))
|
||||
}
|
||||
|
||||
#[allow(clippy::let_underscore_future)]
|
||||
let _ = init_service(my_app());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ use crate::{
|
|||
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.
|
||||
pub struct AppInit<T, B>
|
||||
|
@ -155,7 +155,7 @@ where
|
|||
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 {
|
||||
rmap: Rc<ResourceMap>,
|
||||
config: AppConfig,
|
||||
|
@ -163,6 +163,7 @@ pub(crate) struct AppInitServiceState {
|
|||
}
|
||||
|
||||
impl AppInitServiceState {
|
||||
/// Constructs state collection from resource map and app config.
|
||||
pub(crate) fn new(rmap: Rc<ResourceMap>, config: AppConfig) -> Rc<Self> {
|
||||
Rc::new(AppInitServiceState {
|
||||
rmap,
|
||||
|
@ -171,16 +172,19 @@ impl AppInitServiceState {
|
|||
})
|
||||
}
|
||||
|
||||
/// Returns a reference to the application's resource map.
|
||||
#[inline]
|
||||
pub(crate) fn rmap(&self) -> &ResourceMap {
|
||||
&self.rmap
|
||||
}
|
||||
|
||||
/// Returns a reference to the application's configuration.
|
||||
#[inline]
|
||||
pub(crate) fn config(&self) -> &AppConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
/// Returns a reference to the application's request pool.
|
||||
#[inline]
|
||||
pub(crate) fn pool(&self) -> &HttpRequestPool {
|
||||
&self.pool
|
||||
|
|
|
@ -141,7 +141,7 @@ impl AppConfig {
|
|||
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 {
|
||||
self.addr
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
|
@ -52,12 +52,15 @@ use std::{
|
|||
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 _};
|
||||
|
||||
mod acceptable;
|
||||
mod host;
|
||||
|
||||
pub use self::acceptable::Acceptable;
|
||||
pub use self::host::{Host, HostGuard};
|
||||
|
||||
/// Provides access to request parts that are useful during routing.
|
||||
#[derive(Debug)]
|
||||
|
@ -286,11 +289,25 @@ pub fn Method(method: HttpMethod) -> impl Guard {
|
|||
MethodGuard(method)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct RegisteredMethods(pub(crate) Vec<HttpMethod>);
|
||||
|
||||
/// HTTP method guard.
|
||||
struct MethodGuard(HttpMethod);
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MethodGuard(HttpMethod);
|
||||
|
||||
impl Guard for MethodGuard {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -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)]
|
||||
mod tests {
|
||||
use actix_http::{header, Method};
|
||||
|
@ -501,90 +400,6 @@ mod tests {
|
|||
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]
|
||||
fn method_guards() {
|
||||
let get_req = TestRequest::get().to_srv_request();
|
||||
|
|
|
@ -76,7 +76,6 @@ impl ConnectionInfo {
|
|||
for (name, val) in req
|
||||
.headers
|
||||
.get_all(&header::FORWARDED)
|
||||
.into_iter()
|
||||
.filter_map(|hdr| hdr.to_str().ok())
|
||||
// "for=1.2.3.4, for=5.6.7.8; scheme=https"
|
||||
.flat_map(|val| val.split(';'))
|
||||
|
|
|
@ -69,6 +69,7 @@
|
|||
|
||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||
#![warn(future_incompatible)]
|
||||
#![allow(clippy::uninlined_format_args)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
|
|
@ -7,7 +7,7 @@ use std::{
|
|||
};
|
||||
|
||||
use futures_core::{future::LocalBoxFuture, ready};
|
||||
use futures_util::future::FutureExt as _;
|
||||
use futures_util::FutureExt as _;
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
use crate::{
|
||||
|
|
|
@ -50,6 +50,8 @@ type DefaultHandler<B> = Option<Rc<ErrorHandler<B>>>;
|
|||
/// will pass by unchanged by this middleware.
|
||||
///
|
||||
/// # Examples
|
||||
/// ## Handler Response
|
||||
/// Header
|
||||
/// ```
|
||||
/// use actix_web::http::{header, StatusCode};
|
||||
/// 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))
|
||||
/// .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
|
||||
/// ```
|
||||
/// # use actix_web::http::{header, StatusCode};
|
||||
|
@ -351,7 +375,7 @@ mod tests {
|
|||
use actix_service::IntoService;
|
||||
use actix_utils::future::ok;
|
||||
use bytes::Bytes;
|
||||
use futures_util::future::FutureExt as _;
|
||||
use futures_util::FutureExt as _;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
|
|
|
@ -260,7 +260,7 @@ impl HttpRequest {
|
|||
Ref::map(self.extensions(), |data| data.get().unwrap())
|
||||
}
|
||||
|
||||
/// App config
|
||||
/// Returns a reference to the application's connection configuration.
|
||||
#[inline]
|
||||
pub fn app_config(&self) -> &AppConfig {
|
||||
self.app_state().config()
|
||||
|
|
|
@ -13,8 +13,9 @@ use crate::{
|
|||
body::MessageBody,
|
||||
data::Data,
|
||||
dev::{ensure_leading_slash, AppService, ResourceDef},
|
||||
guard::Guard,
|
||||
guard::{self, Guard},
|
||||
handler::Handler,
|
||||
http::header,
|
||||
route::{Route, RouteService},
|
||||
service::{
|
||||
BoxedHttpService, BoxedHttpServiceFactory, HttpServiceFactory, ServiceRequest,
|
||||
|
@ -40,8 +41,11 @@ use crate::{
|
|||
/// .route(web::get().to(|| HttpResponse::Ok())));
|
||||
/// ```
|
||||
///
|
||||
/// If no matching route could be found, *405* response code get returned. Default behavior could be
|
||||
/// overridden with `default_resource()` method.
|
||||
/// If no matching route is found, [a 405 response is returned with an appropriate Allow header][RFC
|
||||
/// 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> {
|
||||
endpoint: T,
|
||||
rdef: Patterns,
|
||||
|
@ -66,7 +70,19 @@ impl Resource {
|
|||
guards: Vec::new(),
|
||||
app_data: None,
|
||||
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()))
|
||||
}
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
/// sent to the client instead. Unlike [`Scope`](crate::Scope)s, a [`Resource`] does **not**
|
||||
/// inherit its parent's default service.
|
||||
/// If a custom default service is not registered, an empty `405 Method Not Allowed` response
|
||||
/// with an appropriate Allow header will be sent instead.
|
||||
///
|
||||
/// # 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
|
||||
where
|
||||
F: IntoServiceFactory<U, ServiceRequest>,
|
||||
|
@ -606,7 +637,11 @@ mod tests {
|
|||
async fn test_default_resource() {
|
||||
let srv = init_service(
|
||||
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| {
|
||||
ok(r.into_response(HttpResponse::BadRequest()))
|
||||
}),
|
||||
|
@ -621,6 +656,10 @@ mod tests {
|
|||
.to_request();
|
||||
let resp = call_service(&srv, req).await;
|
||||
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(
|
||||
App::new().service(
|
||||
|
|
|
@ -21,7 +21,7 @@ use crate::{Error, HttpRequest, HttpResponse};
|
|||
/// - `HttpResponse` and `HttpResponseBuilder`
|
||||
/// - `Option<R>` where `R: Responder`
|
||||
/// - `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 [u8]`, `Vec<u8>`, `Bytes`, `BytesMut`
|
||||
/// - [`Json<T>`](crate::web::Json) and [`Form<T>`](crate::web::Form) where `T: Serialize`
|
||||
|
|
|
@ -238,11 +238,7 @@ impl ServiceRequest {
|
|||
self.req.connection_info()
|
||||
}
|
||||
|
||||
/// Returns reference to the Path parameters.
|
||||
///
|
||||
/// Params is a container for URL parameters. A variable segment is specified in the form
|
||||
/// `{identifier}`, where the identifier can be used later in a request handler to access the
|
||||
/// matched value for that segment.
|
||||
/// Counterpart to [`HttpRequest::match_info`].
|
||||
#[inline]
|
||||
pub fn match_info(&self) -> &Path<Url> {
|
||||
self.req.match_info()
|
||||
|
@ -267,12 +263,13 @@ impl ServiceRequest {
|
|||
}
|
||||
|
||||
/// Returns a reference to the application's resource map.
|
||||
/// Counterpart to [`HttpRequest::resource_map`].
|
||||
#[inline]
|
||||
pub fn resource_map(&self) -> &ResourceMap {
|
||||
self.req.resource_map()
|
||||
}
|
||||
|
||||
/// Returns a reference to the application's configuration.
|
||||
/// Counterpart to [`HttpRequest::app_config`].
|
||||
#[inline]
|
||||
pub fn app_config(&self) -> &AppConfig {
|
||||
self.req.app_config()
|
||||
|
|
|
@ -10,12 +10,16 @@
|
|||
//! # Calling Test Service
|
||||
//! - [`TestRequest`]
|
||||
//! - [`call_service`]
|
||||
//! - [`try_call_service`]
|
||||
//! - [`call_and_read_body`]
|
||||
//! - [`call_and_read_body_json`]
|
||||
//! - [`try_call_and_read_body_json`]
|
||||
//!
|
||||
//! # Reading Response Payloads
|
||||
//! - [`read_body`]
|
||||
//! - [`try_read_body`]
|
||||
//! - [`read_body_json`]
|
||||
//! - [`try_read_body_json`]
|
||||
|
||||
// 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)]
|
||||
pub use self::test_utils::{
|
||||
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)]
|
||||
|
|
|
@ -100,6 +100,15 @@ where
|
|||
.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
|
||||
///
|
||||
/// # Examples
|
||||
|
@ -185,13 +194,23 @@ pub async fn read_body<B>(res: ServiceResponse<B>) -> Bytes
|
|||
where
|
||||
B: MessageBody,
|
||||
{
|
||||
let body = res.into_body();
|
||||
body::to_bytes(body)
|
||||
try_read_body(res)
|
||||
.await
|
||||
.map_err(Into::<Box<dyn StdError>>::into)
|
||||
.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.
|
||||
///
|
||||
/// # Examples
|
||||
|
@ -240,18 +259,27 @@ where
|
|||
B: MessageBody,
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let body = read_body(res).await;
|
||||
|
||||
serde_json::from_slice(&body).unwrap_or_else(|err| {
|
||||
try_read_body_json(res).await.unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"could not deserialize body into a {}\nerr: {}\nbody: {:?}",
|
||||
"could not deserialize body into a {}\nerr: {}",
|
||||
std::any::type_name::<T>(),
|
||||
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
|
||||
///
|
||||
/// # Examples
|
||||
|
@ -299,8 +327,23 @@ where
|
|||
B: MessageBody,
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let res = call_service(app, req).await;
|
||||
read_body_json(res).await
|
||||
try_call_and_read_body_json(app, req).await.unwrap()
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
|
@ -358,7 +401,7 @@ mod tests {
|
|||
assert_eq!(result, Bytes::from_static(b"delete!"));
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Person {
|
||||
id: String,
|
||||
name: String,
|
||||
|
@ -383,6 +426,26 @@ mod tests {
|
|||
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]
|
||||
async fn test_body_json() {
|
||||
let app = init_service(App::new().service(web::resource("/people").route(
|
||||
|
@ -403,6 +466,27 @@ mod tests {
|
|||
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]
|
||||
async fn test_request_response_form() {
|
||||
let app = init_service(App::new().service(web::resource("/people").route(
|
||||
|
|
|
@ -27,7 +27,7 @@ use crate::{
|
|||
/// # Examples
|
||||
/// ```
|
||||
/// use std::future::Future;
|
||||
/// use futures_util::stream::StreamExt as _;
|
||||
/// use futures_util::StreamExt as _;
|
||||
/// use actix_web::{post, web};
|
||||
///
|
||||
/// // `body: web::Payload` parameter extracts raw payload stream from request
|
||||
|
|
|
@ -177,7 +177,7 @@ where
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use futures_util::stream::StreamExt as _;
|
||||
use futures_util::StreamExt as _;
|
||||
|
||||
use super::*;
|
||||
use crate::test::TestRequest;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
extern crate tls_openssl as openssl;
|
||||
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
# 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
|
||||
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "awc"
|
||||
version = "3.0.1"
|
||||
version = "3.1.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Async HTTP and WebSocket client library"
|
||||
keywords = ["actix", "http", "framework", "async", "web"]
|
||||
|
@ -57,18 +57,18 @@ dangerous-h2c = []
|
|||
[dependencies]
|
||||
actix-codec = "0.5"
|
||||
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-tls = { version = "3", features = ["connect", "uri"] }
|
||||
actix-utils = "3"
|
||||
|
||||
ahash = "0.7"
|
||||
base64 = "0.13"
|
||||
base64 = "0.21"
|
||||
bytes = "1"
|
||||
cfg-if = "1"
|
||||
derive_more = "0.99.5"
|
||||
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] }
|
||||
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
||||
futures-util = { version = "0.3.17", default-features = false, features = ["alloc", "sink"] }
|
||||
h2 = "0.3.9"
|
||||
http = "0.2.5"
|
||||
itoa = "1"
|
||||
|
@ -80,14 +80,14 @@ rand = "0.8"
|
|||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
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 }
|
||||
|
||||
tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
|
||||
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]
|
||||
actix-http = { version = "3", features = ["openssl"] }
|
||||
|
@ -102,12 +102,12 @@ brotli = "3.3.3"
|
|||
const-str = "0.4"
|
||||
env_logger = "0.9"
|
||||
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"
|
||||
rcgen = "0.9"
|
||||
rustls-pemfile = "1"
|
||||
tokio = { version = "1.13.1", features = ["rt-multi-thread", "macros"] }
|
||||
zstd = "0.11"
|
||||
tokio = { version = "1.18.5", features = ["rt-multi-thread", "macros"] }
|
||||
zstd = "0.12"
|
||||
|
||||
[[example]]
|
||||
name = "client"
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
> Async HTTP and WebSocket client library.
|
||||
|
||||
[](https://crates.io/crates/awc)
|
||||
[](https://docs.rs/awc/3.0.1)
|
||||
[](https://docs.rs/awc/3.1.0)
|
||||

|
||||
[](https://deps.rs/crate/awc/3.0.1)
|
||||
[](https://deps.rs/crate/awc/3.1.0)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
## Documentation & Resources
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
use std::error::Error as StdError;
|
||||
|
||||
#[tokio::main]
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use std::{convert::TryFrom, fmt, net::IpAddr, rc::Rc, time::Duration};
|
||||
|
||||
use base64::prelude::*;
|
||||
|
||||
use actix_http::{
|
||||
error::HttpError,
|
||||
header::{self, HeaderMap, HeaderName, TryIntoHeaderPair},
|
||||
|
@ -210,7 +212,7 @@ where
|
|||
};
|
||||
self.add_default_header((
|
||||
header::AUTHORIZATION,
|
||||
format!("Basic {}", base64::encode(&auth)),
|
||||
format!("Basic {}", BASE64_STANDARD.encode(auth)),
|
||||
))
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ use actix_rt::time::{sleep, Sleep};
|
|||
use actix_service::Service;
|
||||
use ahash::AHashMap;
|
||||
use futures_core::future::LocalBoxFuture;
|
||||
use futures_util::FutureExt;
|
||||
use futures_util::FutureExt as _;
|
||||
use http::uri::Authority;
|
||||
use pin_project_lite::pin_project;
|
||||
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
|
||||
|
|
|
@ -83,7 +83,7 @@
|
|||
//! ```no_run
|
||||
//! # #[actix_rt::main]
|
||||
//! # 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()
|
||||
//! .ws("ws://echo.websocket.org")
|
||||
|
@ -105,7 +105,8 @@
|
|||
#![allow(
|
||||
clippy::type_complexity,
|
||||
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_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
|
@ -138,7 +139,7 @@ pub mod http {
|
|||
}
|
||||
|
||||
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::frozen::{FrozenClientRequest, FrozenSendBuilder};
|
||||
pub use self::request::ClientRequest;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use std::{convert::TryFrom, fmt, net, rc::Rc, time::Duration};
|
||||
|
||||
use base64::prelude::*;
|
||||
use bytes::Bytes;
|
||||
use futures_core::Stream;
|
||||
use serde::Serialize;
|
||||
|
@ -238,7 +239,7 @@ impl ClientRequest {
|
|||
|
||||
self.insert_header((
|
||||
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);
|
||||
|
||||
let _ = req.headers_mut();
|
||||
|
||||
#[allow(clippy::let_underscore_future)]
|
||||
let _ = req.send_body("");
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
//!
|
||||
//! ```no_run
|
||||
//! use awc::{Client, ws};
|
||||
//! use futures_util::{sink::SinkExt as _, stream::StreamExt as _};
|
||||
//! use futures_util::{SinkExt as _, StreamExt as _};
|
||||
//!
|
||||
//! #[actix_rt::main]
|
||||
//! async fn main() {
|
||||
|
@ -28,6 +28,8 @@
|
|||
|
||||
use std::{convert::TryFrom, fmt, net::SocketAddr, str};
|
||||
|
||||
use base64::prelude::*;
|
||||
|
||||
use actix_codec::Framed;
|
||||
use actix_http::{ws, Payload, RequestHead};
|
||||
use actix_rt::time::timeout;
|
||||
|
@ -236,7 +238,10 @@ impl WebsocketsRequest {
|
|||
Some(password) => format!("{}:{}", username, password),
|
||||
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
|
||||
|
@ -321,7 +326,7 @@ impl WebsocketsRequest {
|
|||
// 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).
|
||||
let sec_key: [u8; 16] = rand::random();
|
||||
let key = base64::encode(sec_key);
|
||||
let key = BASE64_STANDARD.encode(sec_key);
|
||||
|
||||
self.head.headers.insert(
|
||||
header::SEC_WEBSOCKET_KEY,
|
||||
|
@ -503,6 +508,8 @@ mod tests {
|
|||
.unwrap(),
|
||||
"Bearer someS3cr3tAutht0k3n"
|
||||
);
|
||||
|
||||
#[allow(clippy::let_underscore_future)]
|
||||
let _ = req.connect();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(clippy::uninlined_format_args)]
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
convert::Infallible,
|
||||
|
@ -11,6 +13,7 @@ use std::{
|
|||
};
|
||||
|
||||
use actix_utils::future::ok;
|
||||
use base64::prelude::*;
|
||||
use bytes::Bytes;
|
||||
use cookie::Cookie;
|
||||
use futures_util::stream;
|
||||
|
@ -139,7 +142,7 @@ async fn timeout_override() {
|
|||
|
||||
#[actix_rt::test]
|
||||
async fn response_timeout() {
|
||||
use futures_util::stream::{once, StreamExt as _};
|
||||
use futures_util::{stream::once, StreamExt as _};
|
||||
|
||||
let srv = actix_test::start(|| {
|
||||
App::new().service(web::resource("/").route(web::to(|| async {
|
||||
|
@ -781,7 +784,7 @@ async fn client_basic_auth() {
|
|||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
== format!("Basic {}", base64::encode("username:password"))
|
||||
== format!("Basic {}", BASE64_STANDARD.encode("username:password"))
|
||||
{
|
||||
HttpResponse::Ok()
|
||||
} else {
|
||||
|
|
Loading…
Reference in New Issue