1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-07-10 12:54:20 +02:00

Compare commits

..

12 Commits

37 changed files with 590 additions and 150 deletions

View File

@ -22,10 +22,10 @@ path = "src/lib.rs"
experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"] experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"]
[dependencies] [dependencies]
actix-http = "3.0.0-rc.1" actix-http = "3.0.0-rc.2"
actix-service = "2" actix-service = "2"
actix-utils = "3" actix-utils = "3"
actix-web = { version = "4.0.0-rc.2", default-features = false } actix-web = { version = "4.0.0-rc.3", default-features = false }
askama_escape = "0.10" askama_escape = "0.10"
bitflags = "1" bitflags = "1"
@ -44,5 +44,5 @@ tokio-uring = { version = "0.2", optional = true, features = ["bytes"] }
[dev-dependencies] [dev-dependencies]
actix-rt = "2.2" actix-rt = "2.2"
actix-test = "0.1.0-beta.12" actix-test = "0.1.0-beta.12"
actix-web = "4.0.0-rc.2" actix-web = "4.0.0-rc.3"
tempfile = "3.2" tempfile = "3.2"

View File

@ -75,7 +75,7 @@ pub(crate) fn directory_listing(
if dir.is_visible(&entry) { if dir.is_visible(&entry) {
let entry = entry.unwrap(); let entry = entry.unwrap();
let p = match entry.path().strip_prefix(&dir.path) { let p = match entry.path().strip_prefix(&dir.path) {
Ok(p) if cfg!(windows) => base.join(p).to_string_lossy().replace("\\", "/"), Ok(p) if cfg!(windows) => base.join(p).to_string_lossy().replace('\\', "/"),
Ok(p) => base.join(p).to_string_lossy().into_owned(), Ok(p) => base.join(p).to_string_lossy().into_owned(),
Err(_) => continue, Err(_) => continue,
}; };

View File

@ -51,5 +51,5 @@ tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
tokio = { version = "1.8.4", features = ["sync"] } tokio = { version = "1.8.4", features = ["sync"] }
[dev-dependencies] [dev-dependencies]
actix-web = { version = "4.0.0-rc.2", default-features = false, features = ["cookies"] } actix-web = { version = "4.0.0-rc.3", default-features = false, features = ["cookies"] }
actix-http = "3.0.0-rc.1" actix-http = "3.0.0-rc.2"

View File

@ -3,6 +3,20 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 3.0.0-rc.2 - 2022-02-08
### Added
- Implement `From<Vec<u8>>` for `Response<Vec<u8>>`. [#2625]
### Changed
- `error::DispatcherError` enum is now marked `#[non_exhaustive]`. [#2624]
### Fixed
- Issue where handlers that took payload but then dropped without reading it to EOF it would cause keep-alive connections to become stuck. [#2624]
[#2624]: https://github.com/actix/actix-web/pull/2624
[#2625]: https://github.com/actix/actix-web/pull/2625
## 3.0.0-rc.1 - 2022-01-31 ## 3.0.0-rc.1 - 2022-01-31
### Added ### Added
- Implement `Default` for `KeepAlive`. [#2611] - Implement `Default` for `KeepAlive`. [#2611]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-http" name = "actix-http"
version = "3.0.0-rc.1" version = "3.0.0-rc.2"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>", "Rob Ede <robjtede@icloud.com>",
@ -100,7 +100,7 @@ zstd = { version = "0.10", optional = true }
actix-http-test = { version = "3.0.0-beta.12", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.12", features = ["openssl"] }
actix-server = "2" actix-server = "2"
actix-tls = { version = "3.0.0", features = ["openssl"] } actix-tls = { version = "3.0.0", features = ["openssl"] }
actix-web = "4.0.0-rc.2" actix-web = "4.0.0-rc.3"
async-stream = "0.3" async-stream = "0.3"
criterion = { version = "0.3", features = ["html_reports"] } criterion = { version = "0.3", features = ["html_reports"] }

View File

@ -3,11 +3,11 @@
> HTTP primitives for the Actix ecosystem. > HTTP primitives for the Actix ecosystem.
[![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) [![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http)
[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-rc.1)](https://docs.rs/actix-http/3.0.0-rc.1) [![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-rc.2)](https://docs.rs/actix-http/3.0.0-rc.2)
[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) [![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-http/3.0.0-rc.1/status.svg)](https://deps.rs/crate/actix-http/3.0.0-rc.1) [![dependency status](https://deps.rs/crate/actix-http/3.0.0-rc.2/status.svg)](https://deps.rs/crate/actix-http/3.0.0-rc.2)
[![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http) [![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -104,8 +104,13 @@ impl ServiceConfig {
self.0.date_service.now() self.0.date_service.now()
} }
pub(crate) fn write_date_header(&self, dst: &mut BytesMut, camel_case: bool) { /// Writes date header to `dst` buffer.
let mut buf: [u8; 39] = [0; 39]; ///
/// Low-level method that utilizes the built-in efficient date service, requiring fewer syscalls
/// than normal. Note that a CRLF (`\r\n`) is included in what is written.
#[doc(hidden)]
pub fn write_date_header(&self, dst: &mut BytesMut, camel_case: bool) {
let mut buf: [u8; 37] = [0; 37];
buf[..6].copy_from_slice(if camel_case { b"Date: " } else { b"date: " }); buf[..6].copy_from_slice(if camel_case { b"Date: " } else { b"date: " });
@ -113,7 +118,7 @@ impl ServiceConfig {
.date_service .date_service
.with_date(|date| buf[6..35].copy_from_slice(&date.bytes)); .with_date(|date| buf[6..35].copy_from_slice(&date.bytes));
buf[35..].copy_from_slice(b"\r\n\r\n"); buf[35..].copy_from_slice(b"\r\n");
dst.extend_from_slice(&buf); dst.extend_from_slice(&buf);
} }

View File

@ -340,6 +340,7 @@ impl From<PayloadError> for Error {
/// A set of errors that can occur during dispatching HTTP requests. /// A set of errors that can occur during dispatching HTTP requests.
#[derive(Debug, Display, From)] #[derive(Debug, Display, From)]
#[non_exhaustive]
pub enum DispatchError { pub enum DispatchError {
/// Service error. /// Service error.
#[display(fmt = "Service Error")] #[display(fmt = "Service Error")]
@ -373,6 +374,10 @@ pub enum DispatchError {
#[display(fmt = "Connection shutdown timeout")] #[display(fmt = "Connection shutdown timeout")]
DisconnectTimeout, DisconnectTimeout,
/// Handler dropped payload before reading EOF.
#[display(fmt = "Handler dropped payload before reading EOF")]
HandlerDroppedPayload,
/// Internal error. /// Internal error.
#[display(fmt = "Internal error")] #[display(fmt = "Internal error")]
InternalError, InternalError,

View File

@ -128,7 +128,10 @@ impl Decoder for ClientCodec {
type Error = ParseError; type Error = ParseError;
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> { fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
debug_assert!(!self.inner.payload.is_some(), "Payload decoder is set"); debug_assert!(
self.inner.payload.is_none(),
"Payload decoder should not be set"
);
if let Some((req, payload)) = self.inner.decoder.decode(src)? { if let Some((req, payload)) = self.inner.decoder.decode(src)? {
if let Some(conn_type) = req.conn_type() { if let Some(conn_type) = req.conn_type() {

View File

@ -125,11 +125,13 @@ impl Decoder for Codec {
self.flags.set(Flags::HEAD, head.method == Method::HEAD); self.flags.set(Flags::HEAD, head.method == Method::HEAD);
self.version = head.version; self.version = head.version;
self.conn_type = head.connection_type(); self.conn_type = head.connection_type();
if self.conn_type == ConnectionType::KeepAlive if self.conn_type == ConnectionType::KeepAlive
&& !self.flags.contains(Flags::KEEP_ALIVE_ENABLED) && !self.flags.contains(Flags::KEEP_ALIVE_ENABLED)
{ {
self.conn_type = ConnectionType::Close self.conn_type = ConnectionType::Close
} }
match payload { match payload {
PayloadType::None => self.payload = None, PayloadType::None => self.payload = None,
PayloadType::Payload(pl) => self.payload = Some(pl), PayloadType::Payload(pl) => self.payload = Some(pl),

View File

@ -209,15 +209,16 @@ impl MessageType for Request {
let (len, method, uri, ver, h_len) = { let (len, method, uri, ver, h_len) = {
// SAFETY: // SAFETY:
// Create an uninitialized array of `MaybeUninit`. The `assume_init` is // Create an uninitialized array of `MaybeUninit`. The `assume_init` is safe because the
// safe because the type we are claiming to have initialized here is a // type we are claiming to have initialized here is a bunch of `MaybeUninit`s, which
// bunch of `MaybeUninit`s, which do not require initialization. // do not require initialization.
let mut parsed = unsafe { let mut parsed = unsafe {
MaybeUninit::<[MaybeUninit<httparse::Header<'_>>; MAX_HEADERS]>::uninit() MaybeUninit::<[MaybeUninit<httparse::Header<'_>>; MAX_HEADERS]>::uninit()
.assume_init() .assume_init()
}; };
let mut req = httparse::Request::new(&mut []); let mut req = httparse::Request::new(&mut []);
match req.parse_with_uninit_headers(src, &mut parsed)? { match req.parse_with_uninit_headers(src, &mut parsed)? {
httparse::Status::Complete(len) => { httparse::Status::Complete(len) => {
let method = Method::from_bytes(req.method.unwrap().as_bytes()) let method = Method::from_bytes(req.method.unwrap().as_bytes())
@ -232,6 +233,7 @@ impl MessageType for Request {
(len, method, uri, version, req.headers.len()) (len, method, uri, version, req.headers.len())
} }
httparse::Status::Partial => { httparse::Status::Partial => {
return if src.len() >= MAX_BUFFER_SIZE { return if src.len() >= MAX_BUFFER_SIZE {
trace!("MAX_BUFFER_SIZE unprocessed data reached, closing"); trace!("MAX_BUFFER_SIZE unprocessed data reached, closing");

View File

@ -21,7 +21,7 @@ use crate::{
config::ServiceConfig, config::ServiceConfig,
error::{DispatchError, ParseError, PayloadError}, error::{DispatchError, ParseError, PayloadError},
service::HttpFlow, service::HttpFlow,
Error, Extensions, OnConnectData, Request, Response, StatusCode, ConnectionType, Error, Extensions, OnConnectData, Request, Response, StatusCode,
}; };
use super::{ use super::{
@ -151,7 +151,8 @@ pin_project! {
error: Option<DispatchError>, error: Option<DispatchError>,
#[pin] #[pin]
state: State<S, B, X>, pub(super) state: State<S, B, X>,
// when Some(_) dispatcher is in state of receiving request payload
payload: Option<PayloadSender>, payload: Option<PayloadSender>,
messages: VecDeque<DispatcherMessage>, messages: VecDeque<DispatcherMessage>,
@ -174,7 +175,7 @@ enum DispatcherMessage {
pin_project! { pin_project! {
#[project = StateProj] #[project = StateProj]
enum State<S, B, X> pub(super) enum State<S, B, X>
where where
S: Service<Request>, S: Service<Request>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
@ -194,7 +195,7 @@ where
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
B: MessageBody, B: MessageBody,
{ {
fn is_none(&self) -> bool { pub(super) fn is_none(&self) -> bool {
matches!(self, State::None) matches!(self, State::None)
} }
} }
@ -686,12 +687,74 @@ where
let can_not_read = !self.can_read(cx); let can_not_read = !self.can_read(cx);
// limit amount of non-processed requests // limit amount of non-processed requests
if pipeline_queue_full || can_not_read { if pipeline_queue_full {
return Ok(false); return Ok(false);
} }
let mut this = self.as_mut().project(); let mut this = self.as_mut().project();
if can_not_read {
log::debug!("cannot read request payload");
if let Some(sender) = &this.payload {
// ...maybe handler does not want to read any more payload...
if let PayloadStatus::Dropped = sender.need_read(cx) {
log::debug!("handler dropped payload early; attempt to clean connection");
// ...in which case poll request payload a few times
loop {
match this.codec.decode(this.read_buf)? {
Some(msg) => {
match msg {
// payload decoded did not yield EOF yet
Message::Chunk(Some(_)) => {
// if non-clean connection, next loop iter will detect empty
// read buffer and close connection
}
// connection is in clean state for next request
Message::Chunk(None) => {
log::debug!("connection successfully cleaned");
// reset dispatcher state
let _ = this.payload.take();
this.state.set(State::None);
// break out of payload decode loop
break;
}
// Either whole payload is read and loop is broken or more data
// was expected in which case connection is closed. In both
// situations dispatcher cannot get here.
Message::Item(_) => {
unreachable!("dispatcher is in payload receive state")
}
}
}
// not enough info to decide if connection is going to be clean or not
None => {
log::error!(
"handler did not read whole payload and dispatcher could not \
drain read buf; return 500 and close connection"
);
this.flags.insert(Flags::SHUTDOWN);
let mut res = Response::internal_server_error().drop_body();
res.head_mut().set_connection_type(ConnectionType::Close);
this.messages.push_back(DispatcherMessage::Error(res));
*this.error = Some(DispatchError::HandlerDroppedPayload);
return Ok(true);
}
}
}
}
} else {
// can_not_read and no request payload
return Ok(false);
}
}
let mut updated = false; let mut updated = false;
loop { loop {

View File

@ -1,6 +1,6 @@
use std::{future::Future, str, task::Poll, time::Duration}; use std::{future::Future, str, task::Poll, time::Duration};
use actix_rt::time::sleep; use actix_rt::{pin, time::sleep};
use actix_service::fn_service; use actix_service::fn_service;
use actix_utils::future::{ready, Ready}; use actix_utils::future::{ready, Ready};
use bytes::Bytes; use bytes::Bytes;
@ -53,6 +53,14 @@ fn echo_path_service(
}) })
} }
fn drop_payload_service(
) -> impl Service<Request, Response = Response<&'static str>, Error = Error> {
fn_service(|mut req: Request| async move {
let _ = req.take_payload();
Ok::<_, Error>(Response::with_body(StatusCode::OK, "payload dropped"))
})
}
fn echo_payload_service() -> impl Service<Request, Response = Response<Bytes>, Error = Error> { fn echo_payload_service() -> impl Service<Request, Response = Response<Bytes>, Error = Error> {
fn_service(|mut req: Request| { fn_service(|mut req: Request| {
Box::pin(async move { Box::pin(async move {
@ -89,7 +97,7 @@ async fn late_request() {
None, None,
OnConnectData::default(), OnConnectData::default(),
); );
actix_rt::pin!(h1); pin!(h1);
lazy(|cx| { lazy(|cx| {
assert!(matches!(&h1.inner, DispatcherState::Normal { .. })); assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
@ -156,7 +164,7 @@ async fn oneshot_connection() {
None, None,
OnConnectData::default(), OnConnectData::default(),
); );
actix_rt::pin!(h1); pin!(h1);
lazy(|cx| { lazy(|cx| {
assert!(matches!(&h1.inner, DispatcherState::Normal { .. })); assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
@ -173,13 +181,16 @@ async fn oneshot_connection() {
stabilize_date_header(&mut res); stabilize_date_header(&mut res);
let res = &res[..]; let res = &res[..];
let exp = b"\ let exp = http_msg(
HTTP/1.1 200 OK\r\n\ r"
content-length: 5\r\n\ HTTP/1.1 200 OK
connection: close\r\n\ content-length: 5
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\ connection: close
/abcd\ date: Thu, 01 Jan 1970 12:34:56 UTC
";
/abcd
",
);
assert_eq!( assert_eq!(
res, res,
@ -188,7 +199,7 @@ async fn oneshot_connection() {
response: {:?}\n\ response: {:?}\n\
expected: {:?}", expected: {:?}",
String::from_utf8_lossy(res), String::from_utf8_lossy(res),
String::from_utf8_lossy(exp) String::from_utf8_lossy(&exp)
); );
}) })
.await; .await;
@ -214,7 +225,7 @@ async fn keep_alive_timeout() {
None, None,
OnConnectData::default(), OnConnectData::default(),
); );
actix_rt::pin!(h1); pin!(h1);
lazy(|cx| { lazy(|cx| {
assert!(matches!(&h1.inner, DispatcherState::Normal { .. })); assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
@ -293,7 +304,7 @@ async fn keep_alive_follow_up_req() {
None, None,
OnConnectData::default(), OnConnectData::default(),
); );
actix_rt::pin!(h1); pin!(h1);
lazy(|cx| { lazy(|cx| {
assert!(matches!(&h1.inner, DispatcherState::Normal { .. })); assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
@ -413,7 +424,7 @@ async fn req_parse_err() {
OnConnectData::default(), OnConnectData::default(),
); );
actix_rt::pin!(h1); pin!(h1);
match h1.as_mut().poll(cx) { match h1.as_mut().poll(cx) {
Poll::Pending => panic!(), Poll::Pending => panic!(),
@ -459,7 +470,7 @@ async fn pipelining_ok_then_ok() {
OnConnectData::default(), OnConnectData::default(),
); );
actix_rt::pin!(h1); pin!(h1);
assert!(matches!(&h1.inner, DispatcherState::Normal { .. })); assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
@ -529,7 +540,7 @@ async fn pipelining_ok_then_bad() {
OnConnectData::default(), OnConnectData::default(),
); );
actix_rt::pin!(h1); pin!(h1);
assert!(matches!(&h1.inner, DispatcherState::Normal { .. })); assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
@ -601,7 +612,7 @@ async fn expect_handling() {
", ",
); );
actix_rt::pin!(h1); pin!(h1);
assert!(h1.as_mut().poll(cx).is_pending()); assert!(h1.as_mut().poll(cx).is_pending());
assert!(matches!(&h1.inner, DispatcherState::Normal { .. })); assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
@ -678,7 +689,7 @@ async fn expect_eager() {
", ",
); );
actix_rt::pin!(h1); pin!(h1);
assert!(h1.as_mut().poll(cx).is_ready()); assert!(h1.as_mut().poll(cx).is_ready());
assert!(matches!(&h1.inner, DispatcherState::Normal { .. })); assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
@ -761,7 +772,7 @@ async fn upgrade_handling() {
", ",
); );
actix_rt::pin!(h1); pin!(h1);
assert!(h1.as_mut().poll(cx).is_ready()); assert!(h1.as_mut().poll(cx).is_ready());
assert!(matches!(&h1.inner, DispatcherState::Upgrade { .. })); assert!(matches!(&h1.inner, DispatcherState::Upgrade { .. }));
@ -771,3 +782,192 @@ async fn upgrade_handling() {
}) })
.await; .await;
} }
#[actix_rt::test]
async fn handler_drop_payload() {
let _ = env_logger::try_init();
let mut buf = TestBuffer::new(http_msg(
r"
POST /drop-payload HTTP/1.1
Content-Length: 3
abc
",
));
let services = HttpFlow::new(
drop_payload_service(),
ExpectHandler,
None::<UpgradeHandler>,
);
let h1 = Dispatcher::new(
buf.clone(),
services,
ServiceConfig::default(),
None,
OnConnectData::default(),
);
pin!(h1);
lazy(|cx| {
assert!(h1.as_mut().poll(cx).is_pending());
// polls: manual
assert_eq!(h1.poll_count, 1);
let mut res = BytesMut::from(buf.take_write_buf().as_ref());
stabilize_date_header(&mut res);
let res = &res[..];
let exp = http_msg(
r"
HTTP/1.1 200 OK
content-length: 15
date: Thu, 01 Jan 1970 12:34:56 UTC
payload dropped
",
);
assert_eq!(
res,
exp,
"\nexpected response not in write buffer:\n\
response: {:?}\n\
expected: {:?}",
String::from_utf8_lossy(res),
String::from_utf8_lossy(&exp)
);
if let DispatcherStateProj::Normal { inner } = h1.as_mut().project().inner.project() {
assert!(inner.state.is_none());
}
})
.await;
lazy(|cx| {
// add message that claims to have payload longer than provided
buf.extend_read_buf(http_msg(
r"
POST /drop-payload HTTP/1.1
Content-Length: 200
abc
",
));
assert!(h1.as_mut().poll(cx).is_pending());
// polls: manual => manual
assert_eq!(h1.poll_count, 2);
let mut res = BytesMut::from(buf.take_write_buf().as_ref());
stabilize_date_header(&mut res);
let res = &res[..];
// expect response immediately even though request side has not finished reading payload
let exp = http_msg(
r"
HTTP/1.1 200 OK
content-length: 15
date: Thu, 01 Jan 1970 12:34:56 UTC
payload dropped
",
);
assert_eq!(
res,
exp,
"\nexpected response not in write buffer:\n\
response: {:?}\n\
expected: {:?}",
String::from_utf8_lossy(res),
String::from_utf8_lossy(&exp)
);
})
.await;
lazy(|cx| {
assert!(h1.as_mut().poll(cx).is_ready());
// polls: manual => manual => manual
assert_eq!(h1.poll_count, 3);
let mut res = BytesMut::from(buf.take_write_buf().as_ref());
stabilize_date_header(&mut res);
let res = &res[..];
// expect that unrequested error response is sent back since connection could not be cleaned
let exp = http_msg(
r"
HTTP/1.1 500 Internal Server Error
content-length: 0
connection: close
date: Thu, 01 Jan 1970 12:34:56 UTC
",
);
assert_eq!(
res,
exp,
"\nexpected response not in write buffer:\n\
response: {:?}\n\
expected: {:?}",
String::from_utf8_lossy(res),
String::from_utf8_lossy(&exp)
);
})
.await;
}
fn http_msg(msg: impl AsRef<str>) -> BytesMut {
let mut msg = msg
.as_ref()
.trim()
.split('\n')
.into_iter()
.map(|line| [line.trim_start(), "\r"].concat())
.collect::<Vec<_>>()
.join("\n");
// remove trailing \r
msg.pop();
if !msg.is_empty() && !msg.contains("\r\n\r\n") {
msg.push_str("\r\n\r\n");
}
BytesMut::from(msg.as_bytes())
}
#[test]
fn http_msg_creates_msg() {
assert_eq!(http_msg(r""), "");
assert_eq!(
http_msg(
r"
POST / HTTP/1.1
Content-Length: 3
abc
"
),
"POST / HTTP/1.1\r\nContent-Length: 3\r\n\r\nabc"
);
assert_eq!(
http_msg(
r"
GET / HTTP/1.1
Content-Length: 3
"
),
"GET / HTTP/1.1\r\nContent-Length: 3\r\n\r\n"
);
}

View File

@ -210,14 +210,14 @@ pub(crate) trait MessageType: Sized {
dst.advance_mut(pos); dst.advance_mut(pos);
} }
// optimized date header, set_date writes \r\n
if !has_date { if !has_date {
// optimized date header, write_date_header writes its own \r\n
config.write_date_header(dst, camel_case); config.write_date_header(dst, camel_case);
} else {
// msg eof
dst.extend_from_slice(b"\r\n");
} }
// end-of-headers marker
dst.extend_from_slice(b"\r\n");
Ok(()) Ok(())
} }

View File

@ -285,6 +285,24 @@ impl From<&'static [u8]> for Response<&'static [u8]> {
} }
} }
impl From<Vec<u8>> for Response<Vec<u8>> {
fn from(val: Vec<u8>) -> Self {
let mut res = Response::with_body(StatusCode::OK, val);
let mime = mime::APPLICATION_OCTET_STREAM.try_into_value().unwrap();
res.headers_mut().insert(header::CONTENT_TYPE, mime);
res
}
}
impl From<&Vec<u8>> for Response<Vec<u8>> {
fn from(val: &Vec<u8>) -> Self {
let mut res = Response::with_body(StatusCode::OK, val.clone());
let mime = mime::APPLICATION_OCTET_STREAM.try_into_value().unwrap();
res.headers_mut().insert(header::CONTENT_TYPE, mime);
res
}
}
impl From<String> for Response<String> { impl From<String> for Response<String> {
fn from(val: String) -> Self { fn from(val: String) -> Self {
let mut res = Response::with_body(StatusCode::OK, val); let mut res = Response::with_body(StatusCode::OK, val);

View File

@ -15,7 +15,7 @@ path = "src/lib.rs"
[dependencies] [dependencies]
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-web = { version = "4.0.0-rc.2", default-features = false } actix-web = { version = "4.0.0-rc.3", default-features = false }
bytes = "1" bytes = "1"
derive_more = "0.99.5" derive_more = "0.99.5"
@ -28,7 +28,7 @@ twoway = "0.2"
[dev-dependencies] [dev-dependencies]
actix-rt = "2.2" actix-rt = "2.2"
actix-http = "3.0.0-rc.1" actix-http = "3.0.0-rc.2"
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
tokio = { version = "1.8.4", features = ["sync"] } tokio = { version = "1.8.4", features = ["sync"] }
tokio-stream = "0.1" tokio-stream = "0.1"

View File

@ -145,7 +145,8 @@ macro_rules! register {
concat!("/user/keys"), concat!("/user/keys"),
concat!("/user/keys/", $p1), concat!("/user/keys/", $p1),
]; ];
std::array::IntoIter::new(arr)
IntoIterator::into_iter(arr)
}}; }};
} }
@ -158,7 +159,7 @@ fn call() -> impl Iterator<Item = &'static str> {
"/repos/rust-lang/rust/releases/1.51.0", "/repos/rust-lang/rust/releases/1.51.0",
]; ];
std::array::IntoIter::new(arr) IntoIterator::into_iter(arr)
} }
fn compare_routers(c: &mut Criterion) { fn compare_routers(c: &mut Criterion) {

View File

@ -898,7 +898,7 @@ impl ResourceDef {
} }
let pattern_re_set = RegexSet::new(re_set).unwrap(); let pattern_re_set = RegexSet::new(re_set).unwrap();
let segments = segments.unwrap_or_else(Vec::new); let segments = segments.unwrap_or_default();
( (
PatternType::DynamicSet(pattern_re_set, pattern_data), PatternType::DynamicSet(pattern_re_set, pattern_data),

View File

@ -29,12 +29,12 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"]
[dependencies] [dependencies]
actix-codec = "0.4.1" actix-codec = "0.4.1"
actix-http = "3.0.0-rc.1" actix-http = "3.0.0-rc.2"
actix-http-test = "3.0.0-beta.12" actix-http-test = "3.0.0-beta.12"
actix-rt = "2.1" actix-rt = "2.1"
actix-service = "2.0.0" actix-service = "2.0.0"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-web = { version = "4.0.0-rc.2", default-features = false, features = ["cookies"] } actix-web = { version = "4.0.0-rc.3", default-features = false, features = ["cookies"] }
awc = { version = "3.0.0-beta.20", default-features = false, features = ["cookies"] } awc = { version = "3.0.0-beta.20", default-features = false, features = ["cookies"] }
futures-core = { version = "0.3.7", default-features = false, features = ["std"] } futures-core = { version = "0.3.7", default-features = false, features = ["std"] }

View File

@ -16,8 +16,8 @@ path = "src/lib.rs"
[dependencies] [dependencies]
actix = { version = "0.12.0", default-features = false } actix = { version = "0.12.0", default-features = false }
actix-codec = "0.4.1" actix-codec = "0.4.1"
actix-http = "3.0.0-rc.1" actix-http = "3.0.0-rc.2"
actix-web = { version = "4.0.0-rc.2", default-features = false } actix-web = { version = "4.0.0-rc.3", default-features = false }
bytes = "1" bytes = "1"
bytestring = "1" bytestring = "1"

View File

@ -25,7 +25,7 @@ actix-macros = "0.2.3"
actix-rt = "2.2" actix-rt = "2.2"
actix-test = "0.1.0-beta.12" actix-test = "0.1.0-beta.12"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-web = "4.0.0-rc.2" actix-web = "4.0.0-rc.3"
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
trybuild = "1" trybuild = "1"

View File

@ -152,6 +152,10 @@ method_macro!(Patch, patch);
/// Marks async main function as the Actix Web system entry-point. /// Marks async main function as the Actix Web system entry-point.
/// ///
/// Note that Actix Web also works under `#[tokio::main]` since version 4.0. However, this macro is
/// still necessary for actor support (since actors use a `System`). Read more in the
/// [`actix_web::rt`](https://docs.rs/actix-web/4/actix_web/rt) module docs.
///
/// # Examples /// # Examples
/// ``` /// ```
/// #[actix_web::main] /// #[actix_web::main]

View File

@ -3,6 +3,18 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 4.0.0-rc.3 - 2022-02-08
### Changed
- `middleware::Condition` gained a broader compatibility; `Compat` is needed in fewer cases. [#2635]
### Added
- Implement `Responder` for `Vec<u8>`. [#2625]
- Re-export `KeepAlive` in `http` mod. [#2625]
[#2625]: https://github.com/actix/actix-web/pull/2625
[#2635]: https://github.com/actix/actix-web/pull/2635
## 4.0.0-rc.2 - 2022-02-02 ## 4.0.0-rc.2 - 2022-02-02
### Added ### Added
- On-by-default `macros` feature flag to enable routing and runtime macros. [#2619] - On-by-default `macros` feature flag to enable routing and runtime macros. [#2619]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-web" name = "actix-web"
version = "4.0.0-rc.2" version = "4.0.0-rc.3"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>", "Rob Ede <robjtede@icloud.com>",
@ -71,7 +71,7 @@ actix-service = "2"
actix-utils = "3" actix-utils = "3"
actix-tls = { version = "3", default-features = false, optional = true } actix-tls = { version = "3", default-features = false, optional = true }
actix-http = { version = "3.0.0-rc.1", features = ["http2", "ws"] } actix-http = { version = "3.0.0-rc.2", features = ["http2", "ws"] }
actix-router = "0.5.0-rc.3" actix-router = "0.5.0-rc.3"
actix-web-codegen = { version = "0.5.0-rc.2", optional = true } actix-web-codegen = { version = "0.5.0-rc.2", optional = true }
@ -116,6 +116,7 @@ serde = { version = "1.0", features = ["derive"] }
static_assertions = "1" static_assertions = "1"
tls-openssl = { package = "openssl", version = "0.10.9" } tls-openssl = { package = "openssl", version = "0.10.9" }
tls-rustls = { package = "rustls", version = "0.20.0" } tls-rustls = { package = "rustls", version = "0.20.0" }
tokio = { version = "1.13.1", features = ["rt-multi-thread", "macros"] }
zstd = "0.10" zstd = "0.10"
[[test]] [[test]]

View File

@ -9,6 +9,7 @@ Headings marked with :warning: are **breaking behavioral changes** and will prob
## Table of Contents: ## Table of Contents:
- [MSRV](#msrv) - [MSRV](#msrv)
- [Server Settings](#server-settings)
- [Module Structure](#module-structure) - [Module Structure](#module-structure)
- [`NormalizePath` Middleware :warning:](#normalizepath-middleware-warning) - [`NormalizePath` Middleware :warning:](#normalizepath-middleware-warning)
- [`FromRequest` Trait](#fromrequest-trait) - [`FromRequest` Trait](#fromrequest-trait)
@ -20,6 +21,11 @@ Headings marked with :warning: are **breaking behavioral changes** and will prob
The MSRV of Actix Web has been raised from 1.42 to 1.54. The MSRV of Actix Web has been raised from 1.42 to 1.54.
## Server Settings
Until actix-web v4, actix-server used the total number of available logical cores as the default number of worker threads. The new default number of worker threads for actix-server is the number of [physical CPU cores available](https://github.com/actix/actix-net/commit/3a3d654cea5e55b169f6fd05693b765799733b1b#diff-96893e8cb2125e6eefc96105a8462c4fd834943ef5129ffbead1a114133ebb78). For more information about this change, refer to [this analysis](https://github.com/actix/actix-web/issues/957).
## Module Structure ## Module Structure
Lots of modules has been organized in this release. If a compile error refers to "item XYZ not found in module..." or "module XYZ not found", refer to the [documentation on docs.rs](https://docs.rs/actix-web) to to search for items' new locations. Lots of modules has been organized in this release. If a compile error refers to "item XYZ not found in module..." or "module XYZ not found", refer to the [documentation on docs.rs](https://docs.rs/actix-web) to to search for items' new locations.
@ -29,13 +35,13 @@ Lots of modules has been organized in this release. If a compile error refers to
The default `NormalizePath` behavior now strips trailing slashes by default. This was previously documented to be the case in v3 but the behavior now matches. The effect is that routes defined with trailing slashes will become inaccessible when using `NormalizePath::default()`. As such, calling `NormalizePath::default()` will log a warning. It is advised that the `new` or `trim` methods be used instead. The default `NormalizePath` behavior now strips trailing slashes by default. This was previously documented to be the case in v3 but the behavior now matches. The effect is that routes defined with trailing slashes will become inaccessible when using `NormalizePath::default()`. As such, calling `NormalizePath::default()` will log a warning. It is advised that the `new` or `trim` methods be used instead.
```diff ```diff
- #[get("/test/")]` - #[get("/test/")]
+ #[get("/test")]` + #[get("/test")]
async fn handler() { async fn handler() {
App::new() App::new()
- .wrap(NormalizePath::default())` - .wrap(NormalizePath::default())
+ .wrap(NormalizePath::trim())` + .wrap(NormalizePath::trim())
``` ```
Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`. Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`.
@ -46,10 +52,12 @@ The associated type `Config` of `FromRequest` was removed. If you have custom ex
```diff ```diff
impl FromRequest for MyExtractor { impl FromRequest for MyExtractor {
- `type Config = ();` - type Config = ();
} }
``` ```
Consequently, the `FromRequest::configure` method was also removed. Config for extractors is still provided using `App::app_data` but should now be constructed in a standalone way.
## Compression Feature Flags ## Compression Feature Flags
Feature flag `compress` has been split into its supported algorithm (brotli, gzip, zstd). By default, all compression algorithms are enabled. The new flags are: Feature flag `compress` has been split into its supported algorithm (brotli, gzip, zstd). By default, all compression algorithms are enabled. The new flags are:
@ -131,3 +139,7 @@ TODO
## HttpResponse no longer implements Future ## HttpResponse no longer implements Future
TODO TODO
## `#[actix_web::main]` and `#[tokio::main]`
TODO

View File

@ -6,10 +6,10 @@
<p> <p>
[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) [![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web)
[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-rc.2)](https://docs.rs/actix-web/4.0.0-rc.2) [![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-rc.3)](https://docs.rs/actix-web/4.0.0-rc.3)
![MSRV](https://img.shields.io/badge/rustc-1.54+-ab6000.svg) ![MSRV](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg)
[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-rc.2/status.svg)](https://deps.rs/crate/actix-web/4.0.0-rc.2) [![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-rc.3/status.svg)](https://deps.rs/crate/actix-web/4.0.0-rc.3)
<br /> <br />
[![CI](https://github.com/actix/actix-web/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-web/actions/workflows/ci.yml) [![CI](https://github.com/actix/actix-web/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-web/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web)
@ -32,7 +32,7 @@
- Static assets - Static assets
- SSL support using OpenSSL or Rustls - SSL support using OpenSSL or Rustls
- Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) - Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
- Includes an async [HTTP client](https://docs.rs/awc/) - Integrates with the [`awc` HTTP client](https://docs.rs/awc/)
- Runs on stable Rust 1.54+ - Runs on stable Rust 1.54+
## Documentation ## Documentation

View File

@ -10,12 +10,16 @@ use crate::{
/// The interface for request handlers. /// The interface for request handlers.
/// ///
/// # What Is A Request Handler /// # What Is A Request Handler
/// A request handler has three requirements: /// In short, a handler is just an async function that receives request-based arguments, in any
/// order, and returns something that can be converted to a response.
///
/// In particular, a request handler has three requirements:
/// 1. It is an async function (or a function/closure that returns an appropriate future); /// 1. It is an async function (or a function/closure that returns an appropriate future);
/// 1. The function parameters (up to 12) implement [`FromRequest`]; /// 1. The function parameters (up to 12) implement [`FromRequest`];
/// 1. The async function (or future) resolves to a type that can be converted into an /// 1. The async function (or future) resolves to a type that can be converted into an
/// [`HttpResponse`] (i.e., it implements the [`Responder`] trait). /// [`HttpResponse`] (i.e., it implements the [`Responder`] trait).
/// ///
///
/// # Compiler Errors /// # Compiler Errors
/// If you get the error `the trait Handler<_> is not implemented`, then your handler does not /// If you get the error `the trait Handler<_> is not implemented`, then your handler does not
/// fulfill the _first_ of the above requirements. Missing other requirements manifest as errors on /// fulfill the _first_ of the above requirements. Missing other requirements manifest as errors on

View File

@ -62,18 +62,18 @@ crate::http::header::common_header! {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use actix_http::test::TestRequest;
use super::IfNoneMatch; use super::IfNoneMatch;
use crate::http::header::{EntityTag, Header, IF_NONE_MATCH}; use crate::http::header::{EntityTag, Header, IF_NONE_MATCH};
use actix_http::test::TestRequest;
#[test] #[test]
fn test_if_none_match() { fn test_if_none_match() {
let mut if_none_match: Result<IfNoneMatch, _>;
let req = TestRequest::default() let req = TestRequest::default()
.insert_header((IF_NONE_MATCH, "*")) .insert_header((IF_NONE_MATCH, "*"))
.finish(); .finish();
if_none_match = Header::parse(&req);
let mut if_none_match = IfNoneMatch::parse(&req);
assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Any)); assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Any));
let req = TestRequest::default() let req = TestRequest::default()

View File

@ -3,4 +3,4 @@
pub mod header; pub mod header;
// TODO: figure out how best to expose http::Error vs actix_http::Error // TODO: figure out how best to expose http::Error vs actix_http::Error
pub use actix_http::{uri, ConnectionType, Error, Method, StatusCode, Uri, Version}; pub use actix_http::{uri, ConnectionType, Error, KeepAlive, Method, StatusCode, Uri, Version};

View File

@ -42,28 +42,29 @@
//! and otherwise utilizing them. //! and otherwise utilizing them.
//! //!
//! # Features //! # Features
//! * Supports *HTTP/1.x* and *HTTP/2* //! - Supports HTTP/1.x and HTTP/2
//! * Streaming and pipelining //! - Streaming and pipelining
//! * Keep-alive and slow requests handling //! - Powerful [request routing](https://actix.rs/docs/url-dispatch/) with optional macros
//! * Client/server [WebSockets](https://actix.rs/docs/websockets/) support //! - Full [Tokio](https://tokio.rs) compatibility
//! * Transparent content compression/decompression (br, gzip, deflate, zstd) //! - Keep-alive and slow requests handling
//! * Powerful [request routing](https://actix.rs/docs/url-dispatch/) //! - Client/server [WebSockets](https://actix.rs/docs/websockets/) support
//! * Multipart streams //! - Transparent content compression/decompression (br, gzip, deflate, zstd)
//! * Static assets //! - Multipart streams
//! * SSL support using OpenSSL or Rustls //! - Static assets
//! * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) //! - SSL support using OpenSSL or Rustls
//! * Includes an async [HTTP client](https://docs.rs/awc/) //! - Middlewares ([Logger, Session, CORS, etc](middleware))
//! * Runs on stable Rust 1.54+ //! - Integrates with the [`awc` HTTP client](https://docs.rs/awc/)
//! - Runs on stable Rust 1.54+
//! //!
//! # Crate Features //! # Crate Features
//! * `cookies` - cookies support (enabled by default) //! - `cookies` - cookies support (enabled by default)
//! * `macros` - routing and runtime macros (enabled by default) //! - `macros` - routing and runtime macros (enabled by default)
//! * `compress-brotli` - brotli content encoding compression support (enabled by default) //! - `compress-brotli` - brotli content encoding compression support (enabled by default)
//! * `compress-gzip` - gzip and deflate content encoding compression support (enabled by default) //! - `compress-gzip` - gzip and deflate content encoding compression support (enabled by default)
//! * `compress-zstd` - zstd content encoding compression support (enabled by default) //! - `compress-zstd` - zstd content encoding compression support (enabled by default)
//! * `openssl` - HTTPS support via `openssl` crate, supports `HTTP/2` //! - `openssl` - HTTPS support via `openssl` crate, supports `HTTP/2`
//! * `rustls` - HTTPS support via `rustls` crate, supports `HTTP/2` //! - `rustls` - HTTPS support via `rustls` crate, supports `HTTP/2`
//! * `secure-cookies` - secure cookies support //! - `secure-cookies` - secure cookies support
#![deny(rust_2018_idioms, nonstandard_style)] #![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)] #![warn(future_incompatible)]

View File

@ -1,18 +1,22 @@
//! For middleware documentation, see [`Condition`]. //! For middleware documentation, see [`Condition`].
use std::task::{Context, Poll}; use std::{
future::Future,
pin::Pin,
task::{Context, Poll},
};
use actix_service::{Service, Transform}; use futures_core::{future::LocalBoxFuture, ready};
use actix_utils::future::Either;
use futures_core::future::LocalBoxFuture;
use futures_util::future::FutureExt as _; use futures_util::future::FutureExt as _;
use pin_project_lite::pin_project;
use crate::{
body::EitherBody,
dev::{Service, ServiceResponse, Transform},
};
/// Middleware for conditionally enabling other middleware. /// Middleware for conditionally enabling other middleware.
/// ///
/// The controlled middleware must not change the `Service` interfaces. This means you cannot
/// control such middlewares like `Logger` or `Compress` directly. See the [`Compat`](super::Compat)
/// middleware for a workaround.
///
/// # Examples /// # Examples
/// ``` /// ```
/// use actix_web::middleware::{Condition, NormalizePath}; /// use actix_web::middleware::{Condition, NormalizePath};
@ -36,16 +40,16 @@ impl<T> Condition<T> {
} }
} }
impl<S, T, Req> Transform<S, Req> for Condition<T> impl<S, T, Req, BE, BD, Err> Transform<S, Req> for Condition<T>
where where
S: Service<Req> + 'static, S: Service<Req, Response = ServiceResponse<BD>, Error = Err> + 'static,
T: Transform<S, Req, Response = S::Response, Error = S::Error>, T: Transform<S, Req, Response = ServiceResponse<BE>, Error = Err>,
T::Future: 'static, T::Future: 'static,
T::InitError: 'static, T::InitError: 'static,
T::Transform: 'static, T::Transform: 'static,
{ {
type Response = S::Response; type Response = ServiceResponse<EitherBody<BE, BD>>;
type Error = S::Error; type Error = Err;
type Transform = ConditionMiddleware<T::Transform, S>; type Transform = ConditionMiddleware<T::Transform, S>;
type InitError = T::InitError; type InitError = T::InitError;
type Future = LocalBoxFuture<'static, Result<Self::Transform, Self::InitError>>; type Future = LocalBoxFuture<'static, Result<Self::Transform, Self::InitError>>;
@ -69,14 +73,14 @@ pub enum ConditionMiddleware<E, D> {
Disable(D), Disable(D),
} }
impl<E, D, Req> Service<Req> for ConditionMiddleware<E, D> impl<E, D, Req, BE, BD, Err> Service<Req> for ConditionMiddleware<E, D>
where where
E: Service<Req>, E: Service<Req, Response = ServiceResponse<BE>, Error = Err>,
D: Service<Req, Response = E::Response, Error = E::Error>, D: Service<Req, Response = ServiceResponse<BD>, Error = Err>,
{ {
type Response = E::Response; type Response = ServiceResponse<EitherBody<BE, BD>>;
type Error = E::Error; type Error = Err;
type Future = Either<E::Future, D::Future>; type Future = ConditionMiddlewareFuture<E::Future, D::Future>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
match self { match self {
@ -87,27 +91,59 @@ where
fn call(&self, req: Req) -> Self::Future { fn call(&self, req: Req) -> Self::Future {
match self { match self {
ConditionMiddleware::Enable(service) => Either::left(service.call(req)), ConditionMiddleware::Enable(service) => ConditionMiddlewareFuture::Enabled {
ConditionMiddleware::Disable(service) => Either::right(service.call(req)), fut: service.call(req),
},
ConditionMiddleware::Disable(service) => ConditionMiddlewareFuture::Disabled {
fut: service.call(req),
},
} }
} }
} }
pin_project! {
#[doc(hidden)]
#[project = ConditionProj]
pub enum ConditionMiddlewareFuture<E, D> {
Enabled { #[pin] fut: E, },
Disabled { #[pin] fut: D, },
}
}
impl<E, D, BE, BD, Err> Future for ConditionMiddlewareFuture<E, D>
where
E: Future<Output = Result<ServiceResponse<BE>, Err>>,
D: Future<Output = Result<ServiceResponse<BD>, Err>>,
{
type Output = Result<ServiceResponse<EitherBody<BE, BD>>, Err>;
#[inline]
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let res = match self.project() {
ConditionProj::Enabled { fut } => ready!(fut.poll(cx))?.map_into_left_body(),
ConditionProj::Disabled { fut } => ready!(fut.poll(cx))?.map_into_right_body(),
};
Poll::Ready(Ok(res))
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use actix_service::IntoService; use actix_service::IntoService as _;
use actix_utils::future::ok;
use super::*; use super::*;
use crate::{ use crate::{
body::BoxBody,
dev::{ServiceRequest, ServiceResponse}, dev::{ServiceRequest, ServiceResponse},
error::Result, error::Result,
http::{ http::{
header::{HeaderValue, CONTENT_TYPE}, header::{HeaderValue, CONTENT_TYPE},
StatusCode, StatusCode,
}, },
middleware::{err_handlers::*, Compat}, middleware::{self, ErrorHandlerResponse, ErrorHandlers},
test::{self, TestRequest}, test::{self, TestRequest},
web::Bytes,
HttpResponse, HttpResponse,
}; };
@ -120,40 +156,52 @@ mod tests {
Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
} }
#[test]
fn compat_with_builtin_middleware() {
let _ = Condition::new(true, middleware::Compat::noop());
let _ = Condition::new(true, middleware::Logger::default());
let _ = Condition::new(true, middleware::Compress::default());
let _ = Condition::new(true, middleware::NormalizePath::trim());
let _ = Condition::new(true, middleware::DefaultHeaders::new());
let _ = Condition::new(true, middleware::ErrorHandlers::<BoxBody>::new());
let _ = Condition::new(true, middleware::ErrorHandlers::<Bytes>::new());
}
#[actix_rt::test] #[actix_rt::test]
async fn test_handler_enabled() { async fn test_handler_enabled() {
let srv = |req: ServiceRequest| { let srv = |req: ServiceRequest| async move {
ok(req.into_response(HttpResponse::InternalServerError().finish())) let resp = HttpResponse::InternalServerError().message_body(String::new())?;
Ok(req.into_response(resp))
}; };
let mw = Compat::new( let mw = ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500);
ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500),
);
let mw = Condition::new(true, mw) let mw = Condition::new(true, mw)
.new_transform(srv.into_service()) .new_transform(srv.into_service())
.await .await
.unwrap(); .unwrap();
let resp = test::call_service(&mw, TestRequest::default().to_srv_request()).await;
let resp: ServiceResponse<EitherBody<EitherBody<_, _>, String>> =
test::call_service(&mw, TestRequest::default().to_srv_request()).await;
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001");
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_handler_disabled() { async fn test_handler_disabled() {
let srv = |req: ServiceRequest| { let srv = |req: ServiceRequest| async move {
ok(req.into_response(HttpResponse::InternalServerError().finish())) let resp = HttpResponse::InternalServerError().message_body(String::new())?;
Ok(req.into_response(resp))
}; };
let mw = Compat::new( let mw = ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500);
ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500),
);
let mw = Condition::new(false, mw) let mw = Condition::new(false, mw)
.new_transform(srv.into_service()) .new_transform(srv.into_service())
.await .await
.unwrap(); .unwrap();
let resp = test::call_service(&mw, TestRequest::default().to_srv_request()).await; let resp: ServiceResponse<EitherBody<EitherBody<_, _>, String>> =
test::call_service(&mw, TestRequest::default().to_srv_request()).await;
assert_eq!(resp.headers().get(CONTENT_TYPE), None); assert_eq!(resp.headers().get(CONTENT_TYPE), None);
} }
} }

View File

@ -132,6 +132,7 @@ macro_rules! impl_responder_by_forward_into_base_response {
} }
impl_responder_by_forward_into_base_response!(&'static [u8]); impl_responder_by_forward_into_base_response!(&'static [u8]);
impl_responder_by_forward_into_base_response!(Vec<u8>);
impl_responder_by_forward_into_base_response!(Bytes); impl_responder_by_forward_into_base_response!(Bytes);
impl_responder_by_forward_into_base_response!(BytesMut); impl_responder_by_forward_into_base_response!(BytesMut);

View File

@ -1,9 +1,10 @@
//! A selection of re-exports from [`actix-rt`] and [`tokio`]. //! A selection of re-exports from [`tokio`] and [`actix-rt`].
//! //!
//! [`actix-rt`]: https://docs.rs/actix_rt //! Actix Web runs on [Tokio], providing full[^compat] compatibility with its huge ecosystem of
//! [`tokio`]: https://docs.rs/tokio //! crates. Each of the server's workers uses a single-threaded runtime. Read more about the
//! architecture in [`actix-rt`]'s docs.
//! //!
//! # Running Actix Web Macro-less //! # Running Actix Web Without Macros
//! ```no_run //! ```no_run
//! use actix_web::{middleware, rt, web, App, HttpRequest, HttpServer}; //! use actix_web::{middleware, rt, web, App, HttpRequest, HttpServer};
//! //!
@ -12,19 +13,53 @@
//! "Hello world!\r\n" //! "Hello world!\r\n"
//! } //! }
//! //!
//! # fn main() -> std::io::Result<()> { //! fn main() -> std::io::Result<()> {
//! rt::System::new().block_on( //! rt::System::new().block_on(
//! HttpServer::new(|| {
//! App::new().service(web::resource("/").route(web::get().to(index)))
//! })
//! .bind(("127.0.0.1", 8080))?
//! .run()
//! )
//! }
//! ```
//!
//! # Running Actix Web Using `#[tokio::main]`
//! If you need to run something alongside Actix Web that uses Tokio's work stealing functionality,
//! you can run Actix Web under `#[tokio::main]`. The [`Server`](crate::dev::Server) object returned
//! from [`HttpServer::run`](crate::HttpServer::run) can also be [`spawn`]ed, if preferred.
//!
//! Note that `actix` actor support (and therefore WebSocket support through `actix-web-actors`)
//! still require `#[actix_web::main]` since they require a [`System`] to be set up.
//!
//! ```no_run
//! use actix_web::{get, middleware, rt, web, App, HttpRequest, HttpServer};
//!
//! #[get("/")]
//! async fn index(req: HttpRequest) -> &'static str {
//! println!("REQ: {:?}", req);
//! "Hello world!\r\n"
//! }
//!
//! #[tokio::main]
//! async fn main() -> std::io::Result<()> {
//! HttpServer::new(|| { //! HttpServer::new(|| {
//! App::new() //! App::new().service(index)
//! .wrap(middleware::Logger::default())
//! .service(web::resource("/").route(web::get().to(index)))
//! }) //! })
//! .bind(("127.0.0.1", 8080))? //! .bind(("127.0.0.1", 8080))?
//! .workers(1)
//! .run() //! .run()
//! ) //! .await
//! # } //! }
//! ``` //! ```
//!
//! [^compat]: Crates that use Tokio's [`block_in_place`] will not work with Actix Web. Fortunately,
//! the vast majority of Tokio-based crates do not use it.
//!
//! [`actix-rt`]: https://docs.rs/actix-rt
//! [`tokio`]: https://docs.rs/tokio
//! [Tokio]: https://docs.rs/tokio
//! [`spawn`]: https://docs.rs/tokio/1/tokio/fn.spawn.html
//! [`block_in_place`]: https://docs.rs/tokio/1/tokio/task/fn.block_in_place.html
// In particular: // In particular:
// - Omit the `Arbiter` types because they have limited value here. // - Omit the `Arbiter` types because they have limited value here.

View File

@ -128,7 +128,7 @@ where
/// Set number of workers to start. /// Set number of workers to start.
/// ///
/// By default, server uses number of available logical CPU as thread count. /// By default, the number of available physical CPUs is used as the worker count.
pub fn workers(mut self, num: usize) -> Self { pub fn workers(mut self, num: usize) -> Self {
self.builder = self.builder.workers(num); self.builder = self.builder.workers(num);
self self

View File

@ -60,7 +60,7 @@ dangerous-h2c = []
[dependencies] [dependencies]
actix-codec = "0.4.1" actix-codec = "0.4.1"
actix-service = "2.0.0" actix-service = "2.0.0"
actix-http = { version = "3.0.0-rc.1", features = ["http2", "ws"] } actix-http = { version = "3.0.0-rc.2", features = ["http2", "ws"] }
actix-rt = { version = "2.1", default-features = false } actix-rt = { version = "2.1", default-features = false }
actix-tls = { version = "3.0.0", features = ["connect", "uri"] } actix-tls = { version = "3.0.0", features = ["connect", "uri"] }
actix-utils = "3.0.0" actix-utils = "3.0.0"
@ -93,13 +93,13 @@ tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features
trust-dns-resolver = { version = "0.20.0", optional = true } trust-dns-resolver = { version = "0.20.0", optional = true }
[dev-dependencies] [dev-dependencies]
actix-http = { version = "3.0.0-rc.1", features = ["openssl"] } actix-http = { version = "3.0.0-rc.2", features = ["openssl"] }
actix-http-test = { version = "3.0.0-beta.12", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.12", features = ["openssl"] }
actix-server = "2" actix-server = "2"
actix-test = { version = "0.1.0-beta.12", features = ["openssl", "rustls"] } actix-test = { version = "0.1.0-beta.12", features = ["openssl", "rustls"] }
actix-tls = { version = "3.0.0", features = ["openssl", "rustls"] } actix-tls = { version = "3.0.0", features = ["openssl", "rustls"] }
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-web = { version = "4.0.0-rc.2", features = ["openssl"] } actix-web = { version = "4.0.0-rc.3", features = ["openssl"] }
brotli = "3.3.3" brotli = "3.3.3"
const-str = "0.3" const-str = "0.3"

View File

@ -337,7 +337,7 @@ where
match self.get_mut() { match self.get_mut() {
Connection::Tcp(ConnectionType::H1(conn)) => Pin::new(conn).poll_write(cx, buf), Connection::Tcp(ConnectionType::H1(conn)) => Pin::new(conn).poll_write(cx, buf),
Connection::Tls(ConnectionType::H1(conn)) => Pin::new(conn).poll_write(cx, buf), Connection::Tls(ConnectionType::H1(conn)) => Pin::new(conn).poll_write(cx, buf),
_ => unreachable!(H2_UNREACHABLE_WRITE), _ => unreachable!("{}", H2_UNREACHABLE_WRITE),
} }
} }
@ -345,7 +345,7 @@ where
match self.get_mut() { match self.get_mut() {
Connection::Tcp(ConnectionType::H1(conn)) => Pin::new(conn).poll_flush(cx), Connection::Tcp(ConnectionType::H1(conn)) => Pin::new(conn).poll_flush(cx),
Connection::Tls(ConnectionType::H1(conn)) => Pin::new(conn).poll_flush(cx), Connection::Tls(ConnectionType::H1(conn)) => Pin::new(conn).poll_flush(cx),
_ => unreachable!(H2_UNREACHABLE_WRITE), _ => unreachable!("{}", H2_UNREACHABLE_WRITE),
} }
} }
@ -353,7 +353,7 @@ where
match self.get_mut() { match self.get_mut() {
Connection::Tcp(ConnectionType::H1(conn)) => Pin::new(conn).poll_shutdown(cx), Connection::Tcp(ConnectionType::H1(conn)) => Pin::new(conn).poll_shutdown(cx),
Connection::Tls(ConnectionType::H1(conn)) => Pin::new(conn).poll_shutdown(cx), Connection::Tls(ConnectionType::H1(conn)) => Pin::new(conn).poll_shutdown(cx),
_ => unreachable!(H2_UNREACHABLE_WRITE), _ => unreachable!("{}", H2_UNREACHABLE_WRITE),
} }
} }
@ -369,7 +369,7 @@ where
Connection::Tls(ConnectionType::H1(conn)) => { Connection::Tls(ConnectionType::H1(conn)) => {
Pin::new(conn).poll_write_vectored(cx, bufs) Pin::new(conn).poll_write_vectored(cx, bufs)
} }
_ => unreachable!(H2_UNREACHABLE_WRITE), _ => unreachable!("{}", H2_UNREACHABLE_WRITE),
} }
} }
@ -377,7 +377,7 @@ where
match *self { match *self {
Connection::Tcp(ConnectionType::H1(ref conn)) => conn.is_write_vectored(), Connection::Tcp(ConnectionType::H1(ref conn)) => conn.is_write_vectored(),
Connection::Tls(ConnectionType::H1(ref conn)) => conn.is_write_vectored(), Connection::Tls(ConnectionType::H1(ref conn)) => conn.is_write_vectored(),
_ => unreachable!(H2_UNREACHABLE_WRITE), _ => unreachable!("{}", H2_UNREACHABLE_WRITE),
} }
} }
} }

View File

@ -9,7 +9,16 @@ unreleased_for() {
DIR=$1 DIR=$1
CARGO_MANIFEST=$DIR/Cargo.toml CARGO_MANIFEST=$DIR/Cargo.toml
CHANGELOG_FILE=$DIR/CHANGES.md
# determine changelog file name
if [ -f "$DIR/CHANGES.md" ]; then
CHANGELOG_FILE=$DIR/CHANGES.md
elif [ -f "$DIR/CHANGELOG.md" ]; then
CHANGELOG_FILE=$DIR/CHANGELOG.md
else
echo "No changelog file found"
exit 1
fi
# get current version # get current version
PACKAGE_NAME="$(sed -nE 's/^name ?= ?"([^"]+)"$/\1/ p' "$CARGO_MANIFEST" | head -n 1)" PACKAGE_NAME="$(sed -nE 's/^name ?= ?"([^"]+)"$/\1/ p' "$CARGO_MANIFEST" | head -n 1)"
@ -36,6 +45,6 @@ unreleased_for() {
cat "$CHANGE_CHUNK_FILE" cat "$CHANGE_CHUNK_FILE"
} }
for f in $(fd --absolute-path CHANGES.md); do for f in $(fd --absolute-path 'CHANGE\w+.md'); do
unreleased_for $(dirname $f) unreleased_for $(dirname $f)
done done