Merge branch 'master' into I_NEED_MY_TEA

This commit is contained in:
Rob Ede 2023-07-20 10:49:49 +01:00 committed by GitHub
commit 15ae5db9c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
158 changed files with 1605 additions and 1453 deletions

12
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,12 @@
version: 2
updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 10
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 10

View File

@ -26,39 +26,26 @@ jobs:
name: ${{ matrix.target.name }} / ${{ matrix.version }} name: ${{ matrix.target.name }} / ${{ matrix.version }}
runs-on: ${{ matrix.target.os }} runs-on: ${{ matrix.target.os }}
env:
CI: 1
CARGO_INCREMENTAL: 0
VCPKGRS_DYNAMIC: 1
CARGO_UNSTABLE_SPARSE_REGISTRY: true
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
# install OpenSSL on Windows
# TODO: GitHub actions docs state that OpenSSL is
# already installed on these Windows machines somewhere
- name: Set vcpkg root
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Install OpenSSL - name: Install OpenSSL
if: matrix.target.triple == 'x86_64-pc-windows-msvc' if: matrix.target.os == 'windows-latest'
run: vcpkg install openssl:x64-windows run: choco install openssl -y --forcex64 --no-progress
- name: Set OpenSSL dir in env
- name: Install ${{ matrix.version }} if: matrix.target.os == 'windows-latest'
run: | run: |
rustup set profile minimal echo 'OPENSSL_DIR=C:\Program Files\OpenSSL-Win64' | Out-File -FilePath $env:GITHUB_ENV -Append
rustup install ${{ matrix.version }} echo 'OPENSSL_DIR=C:\Program Files\OpenSSL' | Out-File -FilePath $env:GITHUB_ENV -Append
rustup override set ${{ matrix.version }}
- name: Install Rust (${{ matrix.version }})
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: ${{ matrix.version }}
- name: Install cargo-hack - name: Install cargo-hack
uses: taiki-e/install-action@cargo-hack uses: taiki-e/install-action@cargo-hack
- name: Generate Cargo.lock
run: cargo generate-lockfile
- name: Cache Dependencies
uses: Swatinem/rust-cache@v2.2.1
- name: check minimal - name: check minimal
run: cargo ci-check-min run: cargo ci-check-min
@ -88,23 +75,15 @@ jobs:
name: Verify Feature Combinations name: Verify Feature Combinations
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
CI: 1
CARGO_INCREMENTAL: 0
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable - name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Install cargo-hack - name: Install cargo-hack
uses: taiki-e/install-action@cargo-hack uses: taiki-e/install-action@cargo-hack
- name: Generate Cargo.lock
run: cargo generate-lockfile
- name: Cache Dependencies
uses: Swatinem/rust-cache@v2.2.1
- name: check feature combinations - name: check feature combinations
run: cargo ci-check-all-feature-powerset run: cargo ci-check-all-feature-powerset
@ -115,22 +94,14 @@ jobs:
name: nextest name: nextest
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
CI: 1
CARGO_INCREMENTAL: 0
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable - name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Install nextest - name: Install nextest
uses: taiki-e/install-action@nextest uses: taiki-e/install-action@nextest
- name: Generate Cargo.lock
run: cargo generate-lockfile
- name: Cache Dependencies
uses: Swatinem/rust-cache@v2.2.1
- name: Test with cargo-nextest - name: Test with cargo-nextest
run: cargo nextest run run: cargo nextest run

View File

@ -29,17 +29,17 @@ jobs:
name: ${{ matrix.target.name }} / ${{ matrix.version }} name: ${{ matrix.target.name }} / ${{ matrix.version }}
runs-on: ${{ matrix.target.os }} runs-on: ${{ matrix.target.os }}
env: {}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Install OpenSSL - name: Install OpenSSL
if: matrix.target.os == 'windows-latest' if: matrix.target.os == 'windows-latest'
run: choco install openssl run: choco install openssl -y --forcex64 --no-progress
- name: Set OpenSSL dir in env - name: Set OpenSSL dir in env
if: matrix.target.os == 'windows-latest' if: matrix.target.os == 'windows-latest'
run: echo 'OPENSSL_DIR=C:\Program Files\OpenSSL-Win64' | Out-File -FilePath $env:GITHUB_ENV -Append run: |
echo 'OPENSSL_DIR=C:\Program Files\OpenSSL-Win64' | Out-File -FilePath $env:GITHUB_ENV -Append
echo 'OPENSSL_DIR=C:\Program Files\OpenSSL' | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Install Rust (${{ matrix.version }}) - name: Install Rust (${{ matrix.version }})
uses: actions-rust-lang/setup-rust-toolchain@v1 uses: actions-rust-lang/setup-rust-toolchain@v1

View File

@ -29,5 +29,6 @@ jobs:
cargo install cargo-tarpaulin --vers "^0.13" cargo install cargo-tarpaulin --vers "^0.13"
cargo tarpaulin --workspace --features=rustls,openssl --out Xml --verbose cargo tarpaulin --workspace --features=rustls,openssl --out Xml --verbose
- name: Upload to Codecov - name: Upload to Codecov
uses: codecov/codecov-action@v1 uses: codecov/codecov-action@v3.1.4
with: { file: cobertura.xml } with:
file: cobertura.xml

View File

@ -32,7 +32,7 @@ jobs:
run: echo '<meta http-equiv="refresh" content="0;url=actix_web/index.html">' > target/doc/index.html run: echo '<meta http-equiv="refresh" content="0;url=actix_web/index.html">' > target/doc/index.html
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages
uses: JamesIves/github-pages-deploy-action@v4.4.1 uses: JamesIves/github-pages-deploy-action@v4.4.3
with: with:
folder: target/doc folder: target/doc
single-commit: true single-commit: true

3
.rustfmt.toml Normal file
View File

@ -0,0 +1,3 @@
group_imports = "StdExternalCrate"
imports_granularity = "Crate"
use_field_init_shorthand = true

View File

@ -7,11 +7,10 @@ use std::{
}; };
use actix_web::{error::Error, web::Bytes}; use actix_web::{error::Error, web::Bytes};
use futures_core::{ready, Stream};
use pin_project_lite::pin_project;
#[cfg(feature = "experimental-io-uring")] #[cfg(feature = "experimental-io-uring")]
use bytes::BytesMut; use bytes::BytesMut;
use futures_core::{ready, Stream};
use pin_project_lite::pin_project;
use super::named::File; use super::named::File;

View File

@ -1,4 +1,9 @@
use std::{fmt::Write, fs::DirEntry, io, path::Path, path::PathBuf}; use std::{
fmt::Write,
fs::DirEntry,
io,
path::{Path, PathBuf},
};
use actix_web::{dev::ServiceResponse, HttpRequest, HttpResponse}; use actix_web::{dev::ServiceResponse, HttpRequest, HttpResponse};
use percent_encoding::{utf8_percent_encode, CONTROLS}; use percent_encoding::{utf8_percent_encode, CONTROLS};

View File

@ -8,8 +8,7 @@ use std::{
use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt}; use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt};
use actix_web::{ use actix_web::{
dev::{ dev::{
AppService, HttpServiceFactory, RequestHead, ResourceDef, ServiceRequest, AppService, HttpServiceFactory, RequestHead, ResourceDef, ServiceRequest, ServiceResponse,
ServiceResponse,
}, },
error::Error, error::Error,
guard::Guard, guard::Guard,
@ -301,12 +300,8 @@ impl Files {
pub fn default_handler<F, U>(mut self, f: F) -> Self pub fn default_handler<F, U>(mut self, f: F) -> Self
where where
F: IntoServiceFactory<U, ServiceRequest>, F: IntoServiceFactory<U, ServiceRequest>,
U: ServiceFactory< U: ServiceFactory<ServiceRequest, Config = (), Response = ServiceResponse, Error = Error>
ServiceRequest, + 'static,
Config = (),
Response = ServiceResponse,
Error = Error,
> + 'static,
{ {
// create and configure default resource // create and configure default resource
self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::factory( self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::factory(
@ -422,10 +417,14 @@ mod tests {
assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
let body = test::read_body(res).await; let body = test::read_body(res).await;
let body_str = std::str::from_utf8(&body).unwrap();
let actual_path = Path::new(&body_str);
let expected_path = Path::new("actix-files/tests");
assert!( assert!(
body.ends_with(b"actix-files/tests/"), actual_path.ends_with(expected_path),
"body {:?} does not end with `actix-files/tests/`", "body {:?} does not end with {:?}",
body actual_path,
expected_path
); );
} }
} }

View File

@ -18,6 +18,8 @@
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]
use std::path::Path;
use actix_service::boxed::{BoxService, BoxServiceFactory}; use actix_service::boxed::{BoxService, BoxServiceFactory};
use actix_web::{ use actix_web::{
dev::{RequestHead, ServiceRequest, ServiceResponse}, dev::{RequestHead, ServiceRequest, ServiceResponse},
@ -25,7 +27,6 @@ use actix_web::{
http::header::DispositionType, http::header::DispositionType,
}; };
use mime_guess::from_ext; use mime_guess::from_ext;
use std::path::Path;
mod chunked; mod chunked;
mod directory; mod directory;
@ -37,16 +38,15 @@ mod path_buf;
mod range; mod range;
mod service; mod service;
pub use self::chunked::ChunkedReadFile; pub use self::{
pub use self::directory::Directory; chunked::ChunkedReadFile, directory::Directory, files::Files, named::NamedFile,
pub use self::files::Files; range::HttpRange, service::FilesService,
pub use self::named::NamedFile; };
pub use self::range::HttpRange; use self::{
pub use self::service::FilesService; directory::{directory_listing, DirectoryRenderer},
error::FilesError,
use self::directory::{directory_listing, DirectoryRenderer}; path_buf::PathBufWrap,
use self::error::FilesError; };
use self::path_buf::PathBufWrap;
type HttpService = BoxService<ServiceRequest, ServiceResponse, Error>; type HttpService = BoxService<ServiceRequest, ServiceResponse, Error>;
type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>;
@ -554,10 +554,9 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_static_files_with_spaces() { async fn test_static_files_with_spaces() {
let srv = test::init_service( let srv =
App::new().service(Files::new("/", ".").index_file("Cargo.toml")), test::init_service(App::new().service(Files::new("/", ".").index_file("Cargo.toml")))
) .await;
.await;
let request = TestRequest::get() let request = TestRequest::get()
.uri("/tests/test%20space.binary") .uri("/tests/test%20space.binary")
.to_request(); .to_request();
@ -667,8 +666,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_static_files() { async fn test_static_files() {
let srv = let srv =
test::init_service(App::new().service(Files::new("/", ".").show_files_listing())) test::init_service(App::new().service(Files::new("/", ".").show_files_listing())).await;
.await;
let req = TestRequest::with_uri("/missing").to_request(); let req = TestRequest::with_uri("/missing").to_request();
let resp = test::call_service(&srv, req).await; let resp = test::call_service(&srv, req).await;
@ -681,8 +679,7 @@ mod tests {
assert_eq!(resp.status(), StatusCode::NOT_FOUND); assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let srv = let srv =
test::init_service(App::new().service(Files::new("/", ".").show_files_listing())) test::init_service(App::new().service(Files::new("/", ".").show_files_listing())).await;
.await;
let req = TestRequest::with_uri("/tests").to_request(); let req = TestRequest::with_uri("/tests").to_request();
let resp = test::call_service(&srv, req).await; let resp = test::call_service(&srv, req).await;
assert_eq!( assert_eq!(

View File

@ -8,13 +8,13 @@ use std::{
use actix_web::{ use actix_web::{
body::{self, BoxBody, SizedStream}, body::{self, BoxBody, SizedStream},
dev::{ dev::{
self, AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory, self, AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory, ServiceRequest,
ServiceRequest, ServiceResponse, ServiceResponse,
}, },
http::{ http::{
header::{ header::{
self, Charset, ContentDisposition, ContentEncoding, DispositionParam, self, Charset, ContentDisposition, ContentEncoding, DispositionParam, DispositionType,
DispositionType, ExtendedValue, HeaderValue, ExtendedValue, HeaderValue,
}, },
StatusCode, StatusCode,
}, },
@ -85,6 +85,7 @@ pub struct NamedFile {
#[cfg(not(feature = "experimental-io-uring"))] #[cfg(not(feature = "experimental-io-uring"))]
pub(crate) use std::fs::File; pub(crate) use std::fs::File;
#[cfg(feature = "experimental-io-uring")] #[cfg(feature = "experimental-io-uring")]
pub(crate) use tokio_uring::fs::File; pub(crate) use tokio_uring::fs::File;
@ -139,8 +140,7 @@ impl NamedFile {
_ => DispositionType::Attachment, _ => DispositionType::Attachment,
}; };
let mut parameters = let mut parameters = vec![DispositionParam::Filename(String::from(filename.as_ref()))];
vec![DispositionParam::Filename(String::from(filename.as_ref()))];
if !filename.is_ascii() { if !filename.is_ascii() {
parameters.push(DispositionParam::FilenameExt(ExtendedValue { parameters.push(DispositionParam::FilenameExt(ExtendedValue {

View File

@ -48,8 +48,8 @@ impl HttpRange {
/// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`). /// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`).
/// `size` is full size of response (file). /// `size` is full size of response (file).
pub fn parse(header: &str, size: u64) -> Result<Vec<HttpRange>, ParseRangeErr> { pub fn parse(header: &str, size: u64) -> Result<Vec<HttpRange>, ParseRangeErr> {
let ranges = http_range::HttpRange::parse(header, size) let ranges =
.map_err(|err| ParseRangeErr(err.into()))?; http_range::HttpRange::parse(header, size).map_err(|err| ParseRangeErr(err.into()))?;
Ok(ranges Ok(ranges
.iter() .iter()

View File

@ -62,11 +62,7 @@ impl FilesService {
} }
} }
fn serve_named_file( fn serve_named_file(&self, req: ServiceRequest, mut named_file: NamedFile) -> ServiceResponse {
&self,
req: ServiceRequest,
mut named_file: NamedFile,
) -> ServiceResponse {
if let Some(ref mime_override) = self.mime_override { if let Some(ref mime_override) = self.mime_override {
let new_disposition = mime_override(&named_file.content_type.type_()); let new_disposition = mime_override(&named_file.content_type.type_());
named_file.content_disposition.disposition = new_disposition; named_file.content_disposition.disposition = new_disposition;
@ -120,13 +116,11 @@ impl Service<ServiceRequest> for FilesService {
)); ));
} }
let path_on_disk = match PathBufWrap::parse_path( let path_on_disk =
req.match_info().unprocessed(), match PathBufWrap::parse_path(req.match_info().unprocessed(), this.hidden_files) {
this.hidden_files, Ok(item) => item,
) { Err(err) => return Ok(req.error_response(err)),
Ok(item) => item, };
Err(err) => return Ok(req.error_response(err)),
};
if let Some(filter) = &this.path_filter { if let Some(filter) = &this.path_filter {
if !filter(path_on_disk.as_ref(), req.head()) { if !filter(path_on_disk.as_ref(), req.head()) {
@ -177,8 +171,7 @@ impl Service<ServiceRequest> for FilesService {
match NamedFile::open_async(&path).await { match NamedFile::open_async(&path).await {
Ok(mut named_file) => { Ok(mut named_file) => {
if let Some(ref mime_override) = this.mime_override { if let Some(ref mime_override) = this.mime_override {
let new_disposition = let new_disposition = mime_override(&named_file.content_type.type_());
mime_override(&named_file.content_type.type_());
named_file.content_disposition.disposition = new_disposition; named_file.content_disposition.disposition = new_disposition;
} }
named_file.flags = this.file_flags; named_file.flags = this.file_flags;

View File

@ -24,8 +24,7 @@ async fn test_utf8_file_contents() {
// disable UTF-8 attribute // disable UTF-8 attribute
let srv = let srv =
test::init_service(App::new().service(Files::new("/", "./tests").prefer_utf8(false))) test::init_service(App::new().service(Files::new("/", "./tests").prefer_utf8(false))).await;
.await;
let req = TestRequest::with_uri("/utf8.txt").to_request(); let req = TestRequest::with_uri("/utf8.txt").to_request();
let res = test::call_service(&srv, req).await; let res = test::call_service(&srv, req).await;

View File

@ -12,9 +12,7 @@ async fn test_guard_filter() {
let srv = test::init_service( let srv = test::init_service(
App::new() App::new()
.service(Files::new("/", "./tests/fixtures/guards/first").guard(Host("first.com"))) .service(Files::new("/", "./tests/fixtures/guards/first").guard(Host("first.com")))
.service( .service(Files::new("/", "./tests/fixtures/guards/second").guard(Host("second.com"))),
Files::new("/", "./tests/fixtures/guards/second").guard(Host("second.com")),
),
) )
.await; .await;

View File

@ -9,8 +9,7 @@ use actix_web::{
async fn test_directory_traversal_prevention() { async fn test_directory_traversal_prevention() {
let srv = test::init_service(App::new().service(Files::new("/", "./tests"))).await; let srv = test::init_service(App::new().service(Files::new("/", "./tests"))).await;
let req = let req = TestRequest::with_uri("/../../../../../../../../../../../etc/passwd").to_request();
TestRequest::with_uri("/../../../../../../../../../../../etc/passwd").to_request();
let res = test::call_service(&srv, req).await; let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::NOT_FOUND); assert_eq!(res.status(), StatusCode::NOT_FOUND);

View File

@ -23,10 +23,7 @@ async fn main() -> io::Result<()> {
res.insert_header(("x-head", HeaderValue::from_static("dummy value!"))); res.insert_header(("x-head", HeaderValue::from_static("dummy value!")));
let forty_two = req.conn_data::<u32>().unwrap().to_string(); let forty_two = req.conn_data::<u32>().unwrap().to_string();
res.insert_header(( res.insert_header(("x-forty-two", HeaderValue::from_str(&forty_two).unwrap()));
"x-forty-two",
HeaderValue::from_str(&forty_two).unwrap(),
));
Ok::<_, Infallible>(res.body("Hello world!")) Ok::<_, Infallible>(res.body("Hello world!"))
}) })

View File

@ -77,12 +77,8 @@ impl MessageBody for BoxBody {
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
match &mut self.0 { match &mut self.0 {
BoxBodyInner::None(body) => { BoxBodyInner::None(body) => Pin::new(body).poll_next(cx).map_err(|err| match err {}),
Pin::new(body).poll_next(cx).map_err(|err| match err {}) BoxBodyInner::Bytes(body) => Pin::new(body).poll_next(cx).map_err(|err| match err {}),
}
BoxBodyInner::Bytes(body) => {
Pin::new(body).poll_next(cx).map_err(|err| match err {})
}
BoxBodyInner::Stream(body) => Pin::new(body).poll_next(cx), BoxBodyInner::Stream(body) => Pin::new(body).poll_next(cx),
} }
} }

View File

@ -14,12 +14,14 @@ mod size;
mod sized_stream; mod sized_stream;
mod utils; mod utils;
pub use self::body_stream::BodyStream;
pub use self::boxed::BoxBody;
pub use self::either::EitherBody;
pub use self::message_body::MessageBody;
pub(crate) use self::message_body::MessageBodyMapErr; pub(crate) use self::message_body::MessageBodyMapErr;
pub use self::none::None; pub use self::{
pub use self::size::BodySize; body_stream::BodyStream,
pub use self::sized_stream::SizedStream; boxed::BoxBody,
pub use self::utils::{to_bytes, to_bytes_limited, BodyLimitExceeded}; either::EitherBody,
message_body::MessageBody,
none::None,
size::BodySize,
sized_stream::SizedStream,
utils::{to_bytes, to_bytes_limited, BodyLimitExceeded},
};

View File

@ -132,15 +132,15 @@ impl ServiceConfig {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use crate::{date::DATE_VALUE_LENGTH, notify_on_drop};
use actix_rt::{ use actix_rt::{
task::yield_now, task::yield_now,
time::{sleep, sleep_until}, time::{sleep, sleep_until},
}; };
use memchr::memmem; use memchr::memmem;
use super::*;
use crate::{date::DATE_VALUE_LENGTH, notify_on_drop};
#[actix_rt::test] #[actix_rt::test]
async fn test_date_service_update() { async fn test_date_service_update() {
let settings = let settings =

View File

@ -9,11 +9,9 @@ use std::{
use actix_rt::task::{spawn_blocking, JoinHandle}; use actix_rt::task::{spawn_blocking, JoinHandle};
use bytes::Bytes; use bytes::Bytes;
use futures_core::{ready, Stream};
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
use flate2::write::{GzDecoder, ZlibDecoder}; use flate2::write::{GzDecoder, ZlibDecoder};
use futures_core::{ready, Stream};
#[cfg(feature = "compress-zstd")] #[cfg(feature = "compress-zstd")]
use zstd::stream::write::Decoder as ZstdDecoder; use zstd::stream::write::Decoder as ZstdDecoder;
@ -49,9 +47,9 @@ where
))), ))),
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new( ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new(ZlibDecoder::new(
ZlibDecoder::new(Writer::new()), Writer::new(),
))), )))),
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::new(GzDecoder::new( ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::new(GzDecoder::new(

View File

@ -11,12 +11,10 @@ use std::{
use actix_rt::task::{spawn_blocking, JoinHandle}; use actix_rt::task::{spawn_blocking, JoinHandle};
use bytes::Bytes; use bytes::Bytes;
use derive_more::Display; use derive_more::Display;
use futures_core::ready;
use pin_project_lite::pin_project;
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
use flate2::write::{GzEncoder, ZlibEncoder}; use flate2::write::{GzEncoder, ZlibEncoder};
use futures_core::ready;
use pin_project_lite::pin_project;
use tracing::trace; use tracing::trace;
#[cfg(feature = "compress-zstd")] #[cfg(feature = "compress-zstd")]
use zstd::stream::write::Encoder as ZstdEncoder; use zstd::stream::write::Encoder as ZstdEncoder;

View File

@ -7,8 +7,7 @@ use bytes::{Bytes, BytesMut};
mod decoder; mod decoder;
mod encoder; mod encoder;
pub use self::decoder::Decoder; pub use self::{decoder::Decoder, encoder::Encoder};
pub use self::encoder::Encoder;
/// Special-purpose writer for streaming (de-)compression. /// Special-purpose writer for streaming (de-)compression.
/// ///

View File

@ -3,12 +3,11 @@
use std::{error::Error as StdError, fmt, io, str::Utf8Error, string::FromUtf8Error}; use std::{error::Error as StdError, fmt, io, str::Utf8Error, string::FromUtf8Error};
use derive_more::{Display, Error, From}; use derive_more::{Display, Error, From};
pub use http::Error as HttpError;
use http::{uri::InvalidUri, StatusCode}; use http::{uri::InvalidUri, StatusCode};
use crate::{body::BoxBody, Response}; use crate::{body::BoxBody, Response};
pub use http::Error as HttpError;
pub struct Error { pub struct Error {
inner: Box<ErrorInner>, inner: Box<ErrorInner>,
} }

View File

@ -9,9 +9,7 @@ use super::{
decoder::{self, PayloadDecoder, PayloadItem, PayloadType}, decoder::{self, PayloadDecoder, PayloadItem, PayloadType},
encoder, Message, MessageType, encoder, Message, MessageType,
}; };
use crate::{ use crate::{body::BodySize, error::ParseError, ConnectionType, Request, Response, ServiceConfig};
body::BodySize, error::ParseError, ConnectionType, Request, Response, ServiceConfig,
};
bitflags! { bitflags! {
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]

View File

@ -94,9 +94,7 @@ pub(crate) trait MessageType: Sized {
// SAFETY: httparse already checks header value is only visible ASCII bytes // SAFETY: httparse already checks header value is only visible ASCII bytes
// from_maybe_shared_unchecked contains debug assertions so they are omitted here // from_maybe_shared_unchecked contains debug assertions so they are omitted here
let value = unsafe { let value = unsafe {
HeaderValue::from_maybe_shared_unchecked( HeaderValue::from_maybe_shared_unchecked(slice.slice(idx.value.0..idx.value.1))
slice.slice(idx.value.0..idx.value.1),
)
}; };
match name { match name {
@ -275,8 +273,7 @@ impl MessageType for Request {
let mut msg = Request::new(); let mut msg = Request::new();
// convert headers // convert headers
let mut length = let mut length = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len], ver)?;
msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len], ver)?;
// disallow HTTP/1.0 POST requests that do not contain a Content-Length headers // disallow HTTP/1.0 POST requests that do not contain a Content-Length headers
// see https://datatracker.ietf.org/doc/html/rfc1945#section-7.2.2 // see https://datatracker.ietf.org/doc/html/rfc1945#section-7.2.2
@ -356,8 +353,8 @@ impl MessageType for ResponseHead {
Version::HTTP_10 Version::HTTP_10
}; };
let status = StatusCode::from_u16(res.code.unwrap()) let status =
.map_err(|_| ParseError::Status)?; StatusCode::from_u16(res.code.unwrap()).map_err(|_| ParseError::Status)?;
HeaderIndex::record(src, res.headers, &mut headers); HeaderIndex::record(src, res.headers, &mut headers);
(len, version, status, res.headers.len()) (len, version, status, res.headers.len())
@ -378,8 +375,7 @@ impl MessageType for ResponseHead {
msg.version = ver; msg.version = ver;
// convert headers // convert headers
let mut length = let mut length = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len], ver)?;
msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len], ver)?;
// Remove CL value if 0 now that all headers and HTTP/1.0 special cases are processed. // Remove CL value if 0 now that all headers and HTTP/1.0 special cases are processed.
// Protects against some request smuggling attacks. // Protects against some request smuggling attacks.

View File

@ -19,14 +19,6 @@ use tokio::io::{AsyncRead, AsyncWrite};
use tokio_util::codec::{Decoder as _, Encoder as _}; use tokio_util::codec::{Decoder as _, Encoder as _};
use tracing::{error, trace}; use tracing::{error, trace};
use crate::{
body::{BodySize, BoxBody, MessageBody},
config::ServiceConfig,
error::{DispatchError, ParseError, PayloadError},
service::HttpFlow,
Error, Extensions, OnConnectData, Request, Response, StatusCode,
};
use super::{ use super::{
codec::Codec, codec::Codec,
decoder::MAX_BUFFER_SIZE, decoder::MAX_BUFFER_SIZE,
@ -34,6 +26,13 @@ use super::{
timer::TimerState, timer::TimerState,
Message, MessageType, Message, MessageType,
}; };
use crate::{
body::{BodySize, BoxBody, MessageBody},
config::ServiceConfig,
error::{DispatchError, ParseError, PayloadError},
service::HttpFlow,
Error, Extensions, OnConnectData, Request, Response, StatusCode,
};
const LW_BUFFER_SIZE: usize = 1024; const LW_BUFFER_SIZE: usize = 1024;
const HW_BUFFER_SIZE: usize = 1024 * 8; const HW_BUFFER_SIZE: usize = 1024 * 8;
@ -213,9 +212,7 @@ where
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Self::None => write!(f, "State::None"), Self::None => write!(f, "State::None"),
Self::ExpectCall { .. } => { Self::ExpectCall { .. } => f.debug_struct("State::ExpectCall").finish_non_exhaustive(),
f.debug_struct("State::ExpectCall").finish_non_exhaustive()
}
Self::ServiceCall { .. } => { Self::ServiceCall { .. } => {
f.debug_struct("State::ServiceCall").finish_non_exhaustive() f.debug_struct("State::ServiceCall").finish_non_exhaustive()
} }
@ -276,9 +273,7 @@ where
head_timer: TimerState::new(config.client_request_deadline().is_some()), head_timer: TimerState::new(config.client_request_deadline().is_some()),
ka_timer: TimerState::new(config.keep_alive().enabled()), ka_timer: TimerState::new(config.keep_alive().enabled()),
shutdown_timer: TimerState::new( shutdown_timer: TimerState::new(config.client_disconnect_deadline().is_some()),
config.client_disconnect_deadline().is_some(),
),
io: Some(io), io: Some(io),
read_buf: BytesMut::with_capacity(HW_BUFFER_SIZE), read_buf: BytesMut::with_capacity(HW_BUFFER_SIZE),
@ -456,9 +451,7 @@ where
} }
// return with upgrade request and poll it exclusively // return with upgrade request and poll it exclusively
Some(DispatcherMessage::Upgrade(req)) => { Some(DispatcherMessage::Upgrade(req)) => return Ok(PollResponse::Upgrade(req)),
return Ok(PollResponse::Upgrade(req))
}
// all messages are dealt with // all messages are dealt with
None => { None => {
@ -675,9 +668,7 @@ where
} }
_ => { _ => {
unreachable!( unreachable!("State must be set to ServiceCall or ExceptCall in handle_request")
"State must be set to ServiceCall or ExceptCall in handle_request"
)
} }
} }
} }
@ -686,10 +677,7 @@ where
/// Process one incoming request. /// Process one incoming request.
/// ///
/// Returns true if any meaningful work was done. /// Returns true if any meaningful work was done.
fn poll_request( fn poll_request(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Result<bool, DispatchError> {
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Result<bool, DispatchError> {
let pipeline_queue_full = self.messages.len() >= MAX_PIPELINED_MESSAGES; let pipeline_queue_full = self.messages.len() >= MAX_PIPELINED_MESSAGES;
let can_not_read = !self.can_read(cx); let can_not_read = !self.can_read(cx);
@ -859,10 +847,7 @@ where
Ok(()) Ok(())
} }
fn poll_ka_timer( fn poll_ka_timer(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Result<(), DispatchError> {
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Result<(), DispatchError> {
let this = self.as_mut().project(); let this = self.as_mut().project();
if let TimerState::Active { timer } = this.ka_timer { if let TimerState::Active { timer } = this.ka_timer {
debug_assert!( debug_assert!(
@ -927,10 +912,7 @@ where
} }
/// Poll head, keep-alive, and disconnect timer. /// Poll head, keep-alive, and disconnect timer.
fn poll_timers( fn poll_timers(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Result<(), DispatchError> {
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Result<(), DispatchError> {
self.as_mut().poll_head_timer(cx)?; self.as_mut().poll_head_timer(cx)?;
self.as_mut().poll_ka_timer(cx)?; self.as_mut().poll_ka_timer(cx)?;
self.as_mut().poll_shutdown_timer(cx)?; self.as_mut().poll_shutdown_timer(cx)?;
@ -944,10 +926,7 @@ where
/// - `std::io::ErrorKind::ConnectionReset` after partial read; /// - `std::io::ErrorKind::ConnectionReset` after partial read;
/// - all data read done. /// - all data read done.
#[inline(always)] // TODO: bench this inline #[inline(always)] // TODO: bench this inline
fn read_available( fn read_available(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Result<bool, DispatchError> {
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Result<bool, DispatchError> {
let this = self.project(); let this = self.project();
if this.flags.contains(Flags::READ_DISCONNECT) { if this.flags.contains(Flags::READ_DISCONNECT) {

View File

@ -1,14 +1,11 @@
use std::{future::Future, str, task::Poll, time::Duration}; use std::{future::Future, str, task::Poll, time::Duration};
use actix_rt::{pin, time::sleep};
use actix_service::fn_service;
use actix_utils::future::{ready, Ready};
use bytes::Bytes;
use futures_util::future::lazy;
use actix_codec::Framed; use actix_codec::Framed;
use actix_service::Service; use actix_rt::{pin, time::sleep};
use bytes::{Buf, BytesMut}; use actix_service::{fn_service, Service};
use actix_utils::future::{ready, Ready};
use bytes::{Buf, Bytes, BytesMut};
use futures_util::future::lazy;
use super::dispatcher::{Dispatcher, DispatcherState, DispatcherStateProj, Flags}; use super::dispatcher::{Dispatcher, DispatcherState, DispatcherStateProj, Flags};
use crate::{ use crate::{
@ -43,8 +40,8 @@ fn status_service(
fn_service(move |_req: Request| ready(Ok::<_, Error>(Response::new(status)))) fn_service(move |_req: Request| ready(Ok::<_, Error>(Response::new(status))))
} }
fn echo_path_service( fn echo_path_service() -> impl Service<Request, Response = Response<impl MessageBody>, Error = Error>
) -> impl Service<Request, Response = Response<impl MessageBody>, Error = Error> { {
fn_service(|req: Request| { fn_service(|req: Request| {
let path = req.path().as_bytes(); let path = req.path().as_bytes();
ready(Ok::<_, Error>( ready(Ok::<_, Error>(
@ -53,8 +50,8 @@ fn echo_path_service(
}) })
} }
fn drop_payload_service( fn drop_payload_service() -> impl Service<Request, Response = Response<&'static str>, Error = Error>
) -> impl Service<Request, Response = Response<&'static str>, Error = Error> { {
fn_service(|mut req: Request| async move { fn_service(|mut req: Request| async move {
let _ = req.take_payload(); let _ = req.take_payload();
Ok::<_, Error>(Response::with_body(StatusCode::OK, "payload dropped")) Ok::<_, Error>(Response::with_body(StatusCode::OK, "payload dropped"))

View File

@ -17,14 +17,16 @@ mod timer;
mod upgrade; mod upgrade;
mod utils; mod utils;
pub use self::client::{ClientCodec, ClientPayloadCodec}; pub use self::{
pub use self::codec::Codec; client::{ClientCodec, ClientPayloadCodec},
pub use self::dispatcher::Dispatcher; codec::Codec,
pub use self::expect::ExpectHandler; dispatcher::Dispatcher,
pub use self::payload::Payload; expect::ExpectHandler,
pub use self::service::{H1Service, H1ServiceHandler}; payload::Payload,
pub use self::upgrade::UpgradeHandler; service::{H1Service, H1ServiceHandler},
pub use self::utils::SendResponse; upgrade::UpgradeHandler,
utils::SendResponse,
};
#[derive(Debug)] #[derive(Debug)]
/// Codec message /// Codec message

View File

@ -15,6 +15,7 @@ use actix_utils::future::ready;
use futures_core::future::LocalBoxFuture; use futures_core::future::LocalBoxFuture;
use tracing::error; use tracing::error;
use super::{codec::Codec, dispatcher::Dispatcher, ExpectHandler, UpgradeHandler};
use crate::{ use crate::{
body::{BoxBody, MessageBody}, body::{BoxBody, MessageBody},
config::ServiceConfig, config::ServiceConfig,
@ -23,8 +24,6 @@ use crate::{
ConnectCallback, OnConnectData, Request, Response, ConnectCallback, OnConnectData, Request, Response,
}; };
use super::{codec::Codec, dispatcher::Dispatcher, ExpectHandler, UpgradeHandler};
/// `ServiceFactory` implementation for HTTP1 transport /// `ServiceFactory` implementation for HTTP1 transport
pub struct H1Service<T, S, B, X = ExpectHandler, U = UpgradeHandler> { pub struct H1Service<T, S, B, X = ExpectHandler, U = UpgradeHandler> {
srv: S, srv: S,
@ -82,13 +81,8 @@ where
/// Create simple tcp stream service /// Create simple tcp stream service
pub fn tcp( pub fn tcp(
self, self,
) -> impl ServiceFactory< ) -> impl ServiceFactory<TcpStream, Config = (), Response = (), Error = DispatchError, InitError = ()>
TcpStream, {
Config = (),
Response = (),
Error = DispatchError,
InitError = (),
> {
fn_service(|io: TcpStream| { fn_service(|io: TcpStream| {
let peer_addr = io.peer_addr().ok(); let peer_addr = io.peer_addr().ok();
ready(Ok((io, peer_addr))) ready(Ok((io, peer_addr)))
@ -99,8 +93,6 @@ where
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
mod openssl { mod openssl {
use super::*;
use actix_tls::accept::{ use actix_tls::accept::{
openssl::{ openssl::{
reexports::{Error as SslError, SslAcceptor}, reexports::{Error as SslError, SslAcceptor},
@ -109,6 +101,8 @@ mod openssl {
TlsError, TlsError,
}; };
use super::*;
impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U> impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,

View File

@ -23,8 +23,7 @@ use crate::{
mod dispatcher; mod dispatcher;
mod service; mod service;
pub use self::dispatcher::Dispatcher; pub use self::{dispatcher::Dispatcher, service::H2Service};
pub use self::service::H2Service;
/// HTTP/2 peer stream. /// HTTP/2 peer stream.
pub struct Payload { pub struct Payload {
@ -58,10 +57,7 @@ impl Stream for Payload {
} }
} }
pub(crate) fn handshake_with_timeout<T>( pub(crate) fn handshake_with_timeout<T>(io: T, config: &ServiceConfig) -> HandshakeWithTimeout<T>
io: T,
config: &ServiceConfig,
) -> HandshakeWithTimeout<T>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
{ {

View File

@ -16,6 +16,7 @@ use actix_utils::future::ready;
use futures_core::{future::LocalBoxFuture, ready}; use futures_core::{future::LocalBoxFuture, ready};
use tracing::{error, trace}; use tracing::{error, trace};
use super::{dispatcher::Dispatcher, handshake_with_timeout, HandshakeWithTimeout};
use crate::{ use crate::{
body::{BoxBody, MessageBody}, body::{BoxBody, MessageBody},
config::ServiceConfig, config::ServiceConfig,
@ -24,8 +25,6 @@ use crate::{
ConnectCallback, OnConnectData, Request, Response, ConnectCallback, OnConnectData, Request, Response,
}; };
use super::{dispatcher::Dispatcher, handshake_with_timeout, HandshakeWithTimeout};
/// `ServiceFactory` implementation for HTTP/2 transport /// `ServiceFactory` implementation for HTTP/2 transport
pub struct H2Service<T, S, B> { pub struct H2Service<T, S, B> {
srv: S, srv: S,

View File

@ -1120,9 +1120,7 @@ mod tests {
assert!(vals.next().is_none()); assert!(vals.next().is_none());
} }
fn owned_pair<'a>( fn owned_pair<'a>((name, val): (&'a HeaderName, &'a HeaderValue)) -> (HeaderName, HeaderValue) {
(name, val): (&'a HeaderName, &'a HeaderValue),
) -> (HeaderName, HeaderValue) {
(name.clone(), val.clone()) (name.clone(), val.clone())
} }
} }

View File

@ -3,33 +3,30 @@
// declaring new header consts will yield this error // declaring new header consts will yield this error
#![allow(clippy::declare_interior_mutable_const)] #![allow(clippy::declare_interior_mutable_const)]
use percent_encoding::{AsciiSet, CONTROLS};
// re-export from http except header map related items // re-export from http except header map related items
pub use ::http::header::{ pub use ::http::header::{
HeaderName, HeaderValue, InvalidHeaderName, InvalidHeaderValue, ToStrError, HeaderName, HeaderValue, InvalidHeaderName, InvalidHeaderValue, ToStrError,
}; };
// re-export const header names, list is explicit so that any updates to `common` module do not // re-export const header names, list is explicit so that any updates to `common` module do not
// conflict with this set // conflict with this set
pub use ::http::header::{ pub use ::http::header::{
ACCEPT, ACCEPT_CHARSET, ACCEPT_ENCODING, ACCEPT_LANGUAGE, ACCEPT_RANGES, ACCEPT, ACCEPT_CHARSET, ACCEPT_ENCODING, ACCEPT_LANGUAGE, ACCEPT_RANGES,
ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS,
ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_EXPOSE_HEADERS, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_EXPOSE_HEADERS, ACCESS_CONTROL_MAX_AGE,
ACCESS_CONTROL_MAX_AGE, ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD, AGE, ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD, AGE, ALLOW, ALT_SVC,
ALLOW, ALT_SVC, AUTHORIZATION, CACHE_CONTROL, CONNECTION, CONTENT_DISPOSITION, AUTHORIZATION, CACHE_CONTROL, CONNECTION, CONTENT_DISPOSITION, CONTENT_ENCODING,
CONTENT_ENCODING, CONTENT_LANGUAGE, CONTENT_LENGTH, CONTENT_LOCATION, CONTENT_RANGE, CONTENT_LANGUAGE, CONTENT_LENGTH, CONTENT_LOCATION, CONTENT_RANGE, CONTENT_SECURITY_POLICY,
CONTENT_SECURITY_POLICY, CONTENT_SECURITY_POLICY_REPORT_ONLY, CONTENT_TYPE, COOKIE, DATE, CONTENT_SECURITY_POLICY_REPORT_ONLY, CONTENT_TYPE, COOKIE, DATE, DNT, ETAG, EXPECT, EXPIRES,
DNT, ETAG, EXPECT, EXPIRES, FORWARDED, FROM, HOST, IF_MATCH, IF_MODIFIED_SINCE, FORWARDED, FROM, HOST, IF_MATCH, IF_MODIFIED_SINCE, IF_NONE_MATCH, IF_RANGE,
IF_NONE_MATCH, IF_RANGE, IF_UNMODIFIED_SINCE, LAST_MODIFIED, LINK, LOCATION, MAX_FORWARDS, IF_UNMODIFIED_SINCE, LAST_MODIFIED, LINK, LOCATION, MAX_FORWARDS, ORIGIN, PRAGMA,
ORIGIN, PRAGMA, PROXY_AUTHENTICATE, PROXY_AUTHORIZATION, PUBLIC_KEY_PINS, PROXY_AUTHENTICATE, PROXY_AUTHORIZATION, PUBLIC_KEY_PINS, PUBLIC_KEY_PINS_REPORT_ONLY, RANGE,
PUBLIC_KEY_PINS_REPORT_ONLY, RANGE, REFERER, REFERRER_POLICY, REFRESH, RETRY_AFTER, REFERER, REFERRER_POLICY, REFRESH, RETRY_AFTER, SEC_WEBSOCKET_ACCEPT, SEC_WEBSOCKET_EXTENSIONS,
SEC_WEBSOCKET_ACCEPT, SEC_WEBSOCKET_EXTENSIONS, SEC_WEBSOCKET_KEY, SEC_WEBSOCKET_PROTOCOL, SEC_WEBSOCKET_KEY, SEC_WEBSOCKET_PROTOCOL, SEC_WEBSOCKET_VERSION, SERVER, SET_COOKIE,
SEC_WEBSOCKET_VERSION, SERVER, SET_COOKIE, STRICT_TRANSPORT_SECURITY, TE, TRAILER, STRICT_TRANSPORT_SECURITY, TE, TRAILER, TRANSFER_ENCODING, UPGRADE, UPGRADE_INSECURE_REQUESTS,
TRANSFER_ENCODING, UPGRADE, UPGRADE_INSECURE_REQUESTS, USER_AGENT, VARY, VIA, WARNING, USER_AGENT, VARY, VIA, WARNING, WWW_AUTHENTICATE, X_CONTENT_TYPE_OPTIONS,
WWW_AUTHENTICATE, X_CONTENT_TYPE_OPTIONS, X_DNS_PREFETCH_CONTROL, X_FRAME_OPTIONS, X_DNS_PREFETCH_CONTROL, X_FRAME_OPTIONS, X_XSS_PROTECTION,
X_XSS_PROTECTION,
}; };
use percent_encoding::{AsciiSet, CONTROLS};
use crate::{error::ParseError, HttpMessage}; use crate::{error::ParseError, HttpMessage};
@ -43,23 +40,22 @@ mod utils;
pub use self::{ pub use self::{
as_name::AsHeaderName, as_name::AsHeaderName,
// re-export list is explicit so that any updates to `http` do not conflict with this set
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,
},
into_pair::TryIntoHeaderPair, into_pair::TryIntoHeaderPair,
into_value::TryIntoHeaderValue, into_value::TryIntoHeaderValue,
map::HeaderMap, map::HeaderMap,
shared::{ shared::{
parse_extended_value, q, Charset, ContentEncoding, ExtendedValue, HttpDate, parse_extended_value, q, Charset, ContentEncoding, ExtendedValue, HttpDate, LanguageTag,
LanguageTag, Quality, QualityItem, Quality, QualityItem,
}, },
utils::{fmt_comma_delimited, from_comma_delimited, from_one_raw_str, http_percent_encode}, utils::{fmt_comma_delimited, from_comma_delimited, from_one_raw_str, http_percent_encode},
}; };
// re-export list is explicit so that any updates to `http` do not conflict with this set
pub use self::common::{
CACHE_STATUS, CDN_CACHE_CONTROL, CROSS_ORIGIN_EMBEDDER_POLICY, CROSS_ORIGIN_OPENER_POLICY,
CROSS_ORIGIN_RESOURCE_POLICY, PERMISSIONS_POLICY, X_FORWARDED_FOR, X_FORWARDED_HOST,
X_FORWARDED_PROTO,
};
/// An interface for types that already represent a valid header. /// An interface for types that already represent a valid header.
pub trait Header: TryIntoHeaderValue { pub trait Header: TryIntoHeaderValue {
/// Returns the name of the header field. /// Returns the name of the header field.

View File

@ -1,5 +1,7 @@
//! Originally taken from `hyper::header::shared`. //! Originally taken from `hyper::header::shared`.
pub use language_tags::LanguageTag;
mod charset; mod charset;
mod content_encoding; mod content_encoding;
mod extended; mod extended;
@ -7,10 +9,11 @@ mod http_date;
mod quality; mod quality;
mod quality_item; mod quality_item;
pub use self::charset::Charset; pub use self::{
pub use self::content_encoding::ContentEncoding; charset::Charset,
pub use self::extended::{parse_extended_value, ExtendedValue}; content_encoding::ContentEncoding,
pub use self::http_date::HttpDate; extended::{parse_extended_value, ExtendedValue},
pub use self::quality::{q, Quality}; http_date::HttpDate,
pub use self::quality_item::QualityItem; quality::{q, Quality},
pub use language_tags::LanguageTag; quality_item::QualityItem,
};

View File

@ -1,8 +1,7 @@
use std::{cmp, fmt, str}; use std::{cmp, fmt, str};
use crate::error::ParseError;
use super::Quality; use super::Quality;
use crate::error::ParseError;
/// Represents an item with a quality value as defined /// Represents an item with a quality value as defined
/// in [RFC 7231 §5.3.1](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1). /// in [RFC 7231 §5.3.1](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1).

View File

@ -61,9 +61,7 @@ pub trait HttpMessage: Sized {
fn encoding(&self) -> Result<&'static Encoding, ContentTypeError> { fn encoding(&self) -> Result<&'static Encoding, ContentTypeError> {
if let Some(mime_type) = self.mime_type()? { if let Some(mime_type) = self.mime_type()? {
if let Some(charset) = mime_type.get_param("charset") { if let Some(charset) = mime_type.get_param("charset") {
if let Some(enc) = if let Some(enc) = Encoding::for_label_no_replacement(charset.as_str().as_bytes()) {
Encoding::for_label_no_replacement(charset.as_str().as_bytes())
{
Ok(enc) Ok(enc)
} else { } else {
Err(ContentTypeError::UnknownEncoding) Err(ContentTypeError::UnknownEncoding)
@ -146,7 +144,7 @@ mod tests {
.finish(); .finish();
assert_eq!(req.content_type(), "text/plain"); assert_eq!(req.content_type(), "text/plain");
let req = TestRequest::default() let req = TestRequest::default()
.insert_header(("content-type", "application/json; charset=utf=8")) .insert_header(("content-type", "application/json; charset=utf-8"))
.finish(); .finish();
assert_eq!(req.content_type(), "application/json"); assert_eq!(req.content_type(), "application/json");
let req = TestRequest::default().finish(); let req = TestRequest::default().finish();

View File

@ -28,8 +28,7 @@
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]
pub use ::http::{uri, uri::Uri}; pub use ::http::{uri, uri::Uri, Method, StatusCode, Version};
pub use ::http::{Method, StatusCode, Version};
pub mod body; pub mod body;
mod builder; mod builder;
@ -57,22 +56,24 @@ pub mod test;
#[cfg(feature = "ws")] #[cfg(feature = "ws")]
pub mod ws; pub mod ws;
pub use self::builder::HttpServiceBuilder;
pub use self::config::ServiceConfig;
pub use self::error::Error;
pub use self::extensions::Extensions;
pub use self::header::ContentEncoding;
pub use self::http_message::HttpMessage;
pub use self::keep_alive::KeepAlive;
pub use self::message::ConnectionType;
pub use self::message::Message;
#[allow(deprecated)] #[allow(deprecated)]
pub use self::payload::{BoxedPayloadStream, Payload, PayloadStream}; pub use self::payload::PayloadStream;
pub use self::requests::{Request, RequestHead, RequestHeadType};
pub use self::responses::{Response, ResponseBuilder, ResponseHead};
pub use self::service::HttpService;
#[cfg(any(feature = "openssl", feature = "rustls"))] #[cfg(any(feature = "openssl", feature = "rustls"))]
pub use self::service::TlsAcceptorConfig; pub use self::service::TlsAcceptorConfig;
pub use self::{
builder::HttpServiceBuilder,
config::ServiceConfig,
error::Error,
extensions::Extensions,
header::ContentEncoding,
http_message::HttpMessage,
keep_alive::KeepAlive,
message::{ConnectionType, Message},
payload::{BoxedPayloadStream, Payload},
requests::{Request, RequestHead, RequestHeadType},
responses::{Response, ResponseBuilder, ResponseHead},
service::HttpService,
};
/// A major HTTP protocol version. /// A major HTTP protocol version.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]

View File

@ -3,5 +3,7 @@
mod head; mod head;
mod request; mod request;
pub use self::head::{RequestHead, RequestHeadType}; pub use self::{
pub use self::request::Request; head::{RequestHead, RequestHeadType},
request::Request,
};

View File

@ -10,8 +10,7 @@ use std::{
use http::{header, Method, Uri, Version}; use http::{header, Method, Uri, Version};
use crate::{ use crate::{
header::HeaderMap, BoxedPayloadStream, Extensions, HttpMessage, Message, Payload, header::HeaderMap, BoxedPayloadStream, Extensions, HttpMessage, Message, Payload, RequestHead,
RequestHead,
}; };
/// An HTTP request. /// An HTTP request.

View File

@ -5,7 +5,5 @@ mod head;
#[allow(clippy::module_inception)] #[allow(clippy::module_inception)]
mod response; mod response;
pub use self::builder::ResponseBuilder;
pub(crate) use self::head::BoxedResponseHead; pub(crate) use self::head::BoxedResponseHead;
pub use self::head::ResponseHead; pub use self::{builder::ResponseBuilder, head::ResponseHead, response::Response};
pub use self::response::Response;

View File

@ -200,13 +200,8 @@ where
/// The resulting service only supports HTTP/1.x. /// The resulting service only supports HTTP/1.x.
pub fn tcp( pub fn tcp(
self, self,
) -> impl ServiceFactory< ) -> impl ServiceFactory<TcpStream, Config = (), Response = (), Error = DispatchError, InitError = ()>
TcpStream, {
Config = (),
Response = (),
Error = DispatchError,
InitError = (),
> {
fn_service(|io: TcpStream| async { fn_service(|io: TcpStream| async {
let peer_addr = io.peer_addr().ok(); let peer_addr = io.peer_addr().ok();
Ok((io, Protocol::Http1, peer_addr)) Ok((io, Protocol::Http1, peer_addr))
@ -219,13 +214,8 @@ where
#[cfg(feature = "http2")] #[cfg(feature = "http2")]
pub fn tcp_auto_h2c( pub fn tcp_auto_h2c(
self, self,
) -> impl ServiceFactory< ) -> impl ServiceFactory<TcpStream, Config = (), Response = (), Error = DispatchError, InitError = ()>
TcpStream, {
Config = (),
Response = (),
Error = DispatchError,
InitError = (),
> {
fn_service(move |io: TcpStream| async move { fn_service(move |io: TcpStream| async move {
// subset of HTTP/2 preface defined by RFC 9113 §3.4 // 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 // this subset was chosen to maximize likelihood that peeking only once will allow us to
@ -563,10 +553,7 @@ where
} }
} }
pub(super) fn _poll_ready( pub(super) fn _poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Response<BoxBody>>> {
&self,
cx: &mut Context<'_>,
) -> Poll<Result<(), Response<BoxBody>>> {
ready!(self.flow.expect.poll_ready(cx).map_err(Into::into))?; ready!(self.flow.expect.poll_ready(cx).map_err(Into::into))?;
ready!(self.flow.service.poll_ready(cx).map_err(Into::into))?; ready!(self.flow.service.poll_ready(cx).map_err(Into::into))?;
@ -625,10 +612,7 @@ where
}) })
} }
fn call( fn call(&self, (io, proto, peer_addr): (T, Protocol, Option<net::SocketAddr>)) -> Self::Future {
&self,
(io, proto, peer_addr): (T, Protocol, Option<net::SocketAddr>),
) -> Self::Future {
let conn_data = OnConnectData::from_io(&io, self.on_connect_ext.as_deref()); let conn_data = OnConnectData::from_io(&io, self.on_connect_ext.as_deref());
match proto { match proto {

View File

@ -70,15 +70,14 @@ mod inner {
task::{Context, Poll}, task::{Context, Poll},
}; };
use actix_codec::Framed;
use actix_service::{IntoService, Service}; use actix_service::{IntoService, Service};
use futures_core::stream::Stream; use futures_core::stream::Stream;
use local_channel::mpsc; use local_channel::mpsc;
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
use tracing::debug;
use actix_codec::Framed;
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncWrite};
use tokio_util::codec::{Decoder, Encoder}; use tokio_util::codec::{Decoder, Encoder};
use tracing::debug;
use crate::{body::BoxBody, Response}; use crate::{body::BoxBody, Response};
@ -413,9 +412,7 @@ mod inner {
} }
State::Error(_) => { State::Error(_) => {
// flush write buffer // flush write buffer
if !this.framed.is_write_buf_empty() if !this.framed.is_write_buf_empty() && this.framed.flush(cx).is_pending() {
&& this.framed.flush(cx).is_pending()
{
return Poll::Pending; return Poll::Pending;
} }
Poll::Ready(Err(this.state.take_error())) Poll::Ready(Err(this.state.take_error()))

View File

@ -221,9 +221,10 @@ impl Parser {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use bytes::Bytes; use bytes::Bytes;
use super::*;
struct F { struct F {
finished: bool, finished: bool,
opcode: OpCode, opcode: OpCode,

View File

@ -8,8 +8,7 @@ use std::io;
use derive_more::{Display, Error, From}; use derive_more::{Display, Error, From};
use http::{header, Method, StatusCode}; use http::{header, Method, StatusCode};
use crate::body::BoxBody; use crate::{body::BoxBody, header::HeaderValue, RequestHead, Response, ResponseBuilder};
use crate::{header::HeaderValue, RequestHead, Response, ResponseBuilder};
mod codec; mod codec;
mod dispatcher; mod dispatcher;
@ -17,10 +16,12 @@ mod frame;
mod mask; mod mask;
mod proto; mod proto;
pub use self::codec::{Codec, Frame, Item, Message}; pub use self::{
pub use self::dispatcher::Dispatcher; codec::{Codec, Frame, Item, Message},
pub use self::frame::Parser; dispatcher::Dispatcher,
pub use self::proto::{hash_key, CloseCode, CloseReason, OpCode}; frame::Parser,
proto::{hash_key, CloseCode, CloseReason, OpCode},
};
/// WebSocket protocol errors. /// WebSocket protocol errors.
#[derive(Debug, Display, Error, From)] #[derive(Debug, Display, Error, From)]
@ -219,10 +220,8 @@ pub fn handshake_response(req: &RequestHead) -> ResponseBuilder {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{header, Method};
use super::*; use super::*;
use crate::test::TestRequest; use crate::{header, test::TestRequest, Method};
#[test] #[test]
fn test_handshake() { fn test_handshake() {

View File

@ -321,8 +321,7 @@ async fn h2_body_length() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| async { .h2(|_| async {
let body = let body = once(async { Ok::<_, Infallible>(Bytes::from_static(STR.as_ref())) });
once(async { Ok::<_, Infallible>(Bytes::from_static(STR.as_ref())) });
Ok::<_, Infallible>( Ok::<_, Infallible>(
Response::ok().set_body(SizedStream::new(STR.len() as u64, body)), Response::ok().set_body(SizedStream::new(STR.len() as u64, body)),

View File

@ -90,11 +90,9 @@ pub fn get_negotiated_alpn_protocol(
config.alpn_protocols.push(client_alpn_protocol.to_vec()); config.alpn_protocols.push(client_alpn_protocol.to_vec());
let mut sess = rustls::ClientConnection::new( let mut sess =
Arc::new(config), rustls::ClientConnection::new(Arc::new(config), ServerName::try_from("localhost").unwrap())
ServerName::try_from("localhost").unwrap(), .unwrap();
)
.unwrap();
let mut sock = StdTcpStream::connect(addr).unwrap(); let mut sock = StdTcpStream::connect(addr).unwrap();
let mut stream = rustls::Stream::new(&mut sess, &mut sock); let mut stream = rustls::Stream::new(&mut sess, &mut sock);

View File

@ -166,8 +166,7 @@ async fn chunked_payload() {
for chunk_size in chunk_sizes.iter() { for chunk_size in chunk_sizes.iter() {
let mut bytes = Vec::new(); let mut bytes = Vec::new();
let random_bytes: Vec<u8> = let random_bytes: Vec<u8> = (0..*chunk_size).map(|_| rand::random::<u8>()).collect();
(0..*chunk_size).map(|_| rand::random::<u8>()).collect();
bytes.extend(format!("{:X}\r\n", chunk_size).as_bytes()); bytes.extend(format!("{:X}\r\n", chunk_size).as_bytes());
bytes.extend(&random_bytes[..]); bytes.extend(&random_bytes[..]);
@ -352,8 +351,7 @@ async fn http10_keepalive() {
.await; .await;
let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); let mut stream = net::TcpStream::connect(srv.addr()).unwrap();
let _ = let _ = stream.write_all(b"GET /test/tests/test HTTP/1.0\r\nconnection: keep-alive\r\n\r\n");
stream.write_all(b"GET /test/tests/test HTTP/1.0\r\nconnection: keep-alive\r\n\r\n");
let mut data = vec![0; 1024]; let mut data = vec![0; 1024];
let _ = stream.read(&mut data); let _ = stream.read(&mut data);
assert_eq!(&data[..17], b"HTTP/1.0 200 OK\r\n"); assert_eq!(&data[..17], b"HTTP/1.0 200 OK\r\n");
@ -795,8 +793,9 @@ async fn not_modified_spec_h1() {
.map_into_boxed_body(), .map_into_boxed_body(),
// with no content-length // with no content-length
"/body" => Response::with_body(StatusCode::NOT_MODIFIED, "1234") "/body" => {
.map_into_boxed_body(), Response::with_body(StatusCode::NOT_MODIFIED, "1234").map_into_boxed_body()
}
// with manual content-length header and specific None body // with manual content-length header and specific None body
"/cl-none" => { "/cl-none" => {

View File

@ -2,6 +2,7 @@
## Unreleased ## Unreleased
- Update `syn` dependency to `2`.
- Minimum supported Rust version (MSRV) is now 1.65 due to transitive `time` dependency. - Minimum supported Rust version (MSRV) is now 1.65 due to transitive `time` dependency.
## 0.6.0 - 2023-02-26 ## 0.6.0 - 2023-02-26

View File

@ -17,11 +17,11 @@ all-features = true
proc-macro = true proc-macro = true
[dependencies] [dependencies]
darling = "0.14" darling = "0.20"
parse-size = "1" parse-size = "1"
proc-macro2 = "1" proc-macro2 = "1"
quote = "1" quote = "1"
syn = "1" syn = "2"
[dev-dependencies] [dev-dependencies]
actix-multipart = "0.6" actix-multipart = "0.6"

View File

@ -19,7 +19,7 @@ all-features = true
[features] [features]
default = ["tempfile", "derive"] default = ["tempfile", "derive"]
derive = ["actix-multipart-derive"] derive = ["actix-multipart-derive"]
tempfile = ["tempfile-dep", "tokio/fs"] tempfile = ["dep:tempfile", "tokio/fs"]
[dependencies] [dependencies]
actix-multipart-derive = { version = "=0.6.0", optional = true } actix-multipart-derive = { version = "=0.6.0", optional = true }
@ -38,9 +38,8 @@ mime = "0.3"
serde = "1" serde = "1"
serde_json = "1" serde_json = "1"
serde_plain = "1" serde_plain = "1"
# TODO(MSRV 1.60): replace with dep: prefix tempfile = { version = "3.4", optional = true }
tempfile-dep = { package = "tempfile", version = "3.4", optional = true } tokio = { version = "1.24.2", features = ["sync", "io-util"] }
tokio = { version = "1.24.2", features = ["sync"] }
[dev-dependencies] [dev-dependencies]
actix-http = "3" actix-http = "3"

View File

@ -27,11 +27,7 @@ pub struct Bytes {
impl<'t> FieldReader<'t> for Bytes { impl<'t> FieldReader<'t> for Bytes {
type Future = LocalBoxFuture<'t, Result<Self, MultipartError>>; type Future = LocalBoxFuture<'t, Result<Self, MultipartError>>;
fn read_field( fn read_field(_: &'t HttpRequest, mut field: Field, limits: &'t mut Limits) -> Self::Future {
_: &'t HttpRequest,
mut field: Field,
limits: &'t mut Limits,
) -> Self::Future {
Box::pin(async move { Box::pin(async move {
let mut buf = BytesMut::with_capacity(131_072); let mut buf = BytesMut::with_capacity(131_072);

View File

@ -7,13 +7,12 @@ use derive_more::{Deref, DerefMut, Display, Error};
use futures_core::future::LocalBoxFuture; use futures_core::future::LocalBoxFuture;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use super::FieldErrorHandler;
use crate::{ use crate::{
form::{bytes::Bytes, FieldReader, Limits}, form::{bytes::Bytes, FieldReader, Limits},
Field, MultipartError, Field, MultipartError,
}; };
use super::FieldErrorHandler;
/// Deserialize from JSON. /// Deserialize from JSON.
#[derive(Debug, Deref, DerefMut)] #[derive(Debug, Deref, DerefMut)]
pub struct Json<T: DeserializeOwned>(pub T); pub struct Json<T: DeserializeOwned>(pub T);

View File

@ -429,8 +429,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_options() { async fn test_options() {
let srv = let srv = actix_test::start(|| App::new().route("/", web::post().to(test_options_route)));
actix_test::start(|| App::new().route("/", web::post().to(test_options_route)));
let mut form = multipart::Form::default(); let mut form = multipart::Form::default();
form.add_text("field1", "value"); form.add_text("field1", "value");
@ -481,9 +480,7 @@ mod tests {
field3: Text<String>, field3: Text<String>,
} }
async fn test_field_renaming_route( async fn test_field_renaming_route(form: MultipartForm<TestFieldRenaming>) -> impl Responder {
form: MultipartForm<TestFieldRenaming>,
) -> impl Responder {
assert_eq!(&*form.field1, "renamed"); assert_eq!(&*form.field1, "renamed");
assert_eq!(&*form.field2, "field1"); assert_eq!(&*form.field2, "field1");
assert_eq!(&*form.field3, "field3"); assert_eq!(&*form.field3, "field3");
@ -492,9 +489,8 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_field_renaming() { async fn test_field_renaming() {
let srv = actix_test::start(|| { let srv =
App::new().route("/", web::post().to(test_field_renaming_route)) actix_test::start(|| App::new().route("/", web::post().to(test_field_renaming_route)));
});
let mut form = multipart::Form::default(); let mut form = multipart::Form::default();
form.add_text("renamed", "renamed"); form.add_text("renamed", "renamed");
@ -623,9 +619,7 @@ mod tests {
HttpResponse::Ok().finish() HttpResponse::Ok().finish()
} }
async fn test_upload_limits_file( async fn test_upload_limits_file(form: MultipartForm<TestFileUploadLimits>) -> impl Responder {
form: MultipartForm<TestFileUploadLimits>,
) -> impl Responder {
assert!(form.field.size > 0); assert!(form.field.size > 0);
HttpResponse::Ok().finish() HttpResponse::Ok().finish()
} }

View File

@ -11,7 +11,7 @@ use derive_more::{Display, Error};
use futures_core::future::LocalBoxFuture; use futures_core::future::LocalBoxFuture;
use futures_util::TryStreamExt as _; use futures_util::TryStreamExt as _;
use mime::Mime; use mime::Mime;
use tempfile_dep::NamedTempFile; use tempfile::NamedTempFile;
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use super::FieldErrorHandler; use super::FieldErrorHandler;
@ -39,23 +39,20 @@ pub struct TempFile {
impl<'t> FieldReader<'t> for TempFile { impl<'t> FieldReader<'t> for TempFile {
type Future = LocalBoxFuture<'t, Result<Self, MultipartError>>; type Future = LocalBoxFuture<'t, Result<Self, MultipartError>>;
fn read_field( fn read_field(req: &'t HttpRequest, mut field: Field, limits: &'t mut Limits) -> Self::Future {
req: &'t HttpRequest,
mut field: Field,
limits: &'t mut Limits,
) -> Self::Future {
Box::pin(async move { Box::pin(async move {
let config = TempFileConfig::from_req(req); let config = TempFileConfig::from_req(req);
let field_name = field.name().to_owned(); let field_name = field.name().to_owned();
let mut size = 0; let mut size = 0;
let file = config.create_tempfile().map_err(|err| { let file = config
config.map_error(req, &field_name, TempFileError::FileIo(err)) .create_tempfile()
})?; .map_err(|err| config.map_error(req, &field_name, TempFileError::FileIo(err)))?;
let mut file_async = tokio::fs::File::from_std(file.reopen().map_err(|err| { let mut file_async =
config.map_error(req, &field_name, TempFileError::FileIo(err)) tokio::fs::File::from_std(file.reopen().map_err(|err| {
})?); config.map_error(req, &field_name, TempFileError::FileIo(err))
})?);
while let Some(chunk) = field.try_next().await? { while let Some(chunk) = field.try_next().await? {
limits.try_consume_limits(chunk.len(), false)?; limits.try_consume_limits(chunk.len(), false)?;
@ -65,9 +62,10 @@ impl<'t> FieldReader<'t> for TempFile {
})?; })?;
} }
file_async.flush().await.map_err(|err| { file_async
config.map_error(req, &field_name, TempFileError::FileIo(err)) .flush()
})?; .await
.map_err(|err| config.map_error(req, &field_name, TempFileError::FileIo(err)))?;
Ok(TempFile { Ok(TempFile {
file, file,
@ -131,12 +129,7 @@ impl TempFileConfig {
.unwrap_or(&DEFAULT_CONFIG) .unwrap_or(&DEFAULT_CONFIG)
} }
fn map_error( fn map_error(&self, req: &HttpRequest, field_name: &str, err: TempFileError) -> MultipartError {
&self,
req: &HttpRequest,
field_name: &str,
err: TempFileError,
) -> MultipartError {
let source = if let Some(ref err_handler) = self.err_handler { let source = if let Some(ref err_handler) = self.err_handler {
(err_handler)(err, req) (err_handler)(err, req)
} else { } else {

View File

@ -17,5 +17,7 @@ mod server;
pub mod form; pub mod form;
pub use self::error::MultipartError; pub use self::{
pub use self::server::{Field, Multipart}; error::MultipartError,
server::{Field, Multipart},
};

View File

@ -161,8 +161,8 @@ impl InnerMultipart {
for h in hdrs { for h in hdrs {
let name = let name =
HeaderName::try_from(h.name).map_err(|_| ParseError::Header)?; HeaderName::try_from(h.name).map_err(|_| ParseError::Header)?;
let value = HeaderValue::try_from(h.value) let value =
.map_err(|_| ParseError::Header)?; HeaderValue::try_from(h.value).map_err(|_| ParseError::Header)?;
headers.append(name, value); headers.append(name, value);
} }
@ -222,8 +222,7 @@ impl InnerMultipart {
if chunk.len() < boundary.len() { if chunk.len() < boundary.len() {
continue; continue;
} }
if &chunk[..2] == b"--" && &chunk[2..chunk.len() - 2] == boundary.as_bytes() if &chunk[..2] == b"--" && &chunk[2..chunk.len() - 2] == boundary.as_bytes() {
{
break; break;
} else { } else {
if chunk.len() < boundary.len() + 2 { if chunk.len() < boundary.len() + 2 {
@ -268,9 +267,7 @@ impl InnerMultipart {
match field.borrow_mut().poll(safety) { match field.borrow_mut().poll(safety) {
Poll::Pending => return Poll::Pending, Poll::Pending => return Poll::Pending,
Poll::Ready(Some(Ok(_))) => continue, Poll::Ready(Some(Ok(_))) => continue,
Poll::Ready(Some(Err(err))) => { Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err))),
return Poll::Ready(Some(Err(err)))
}
Poll::Ready(None) => true, Poll::Ready(None) => true,
} }
} }
@ -289,8 +286,7 @@ impl InnerMultipart {
match self.state { match self.state {
// read until first boundary // read until first boundary
InnerState::FirstBoundary => { InnerState::FirstBoundary => {
match InnerMultipart::skip_until_boundary(&mut payload, &self.boundary)? match InnerMultipart::skip_until_boundary(&mut payload, &self.boundary)? {
{
Some(eof) => { Some(eof) => {
if eof { if eof {
self.state = InnerState::Eof; self.state = InnerState::Eof;
@ -667,9 +663,7 @@ impl InnerField {
Ok(None) => Poll::Pending, Ok(None) => Poll::Pending,
Ok(Some(line)) => { Ok(Some(line)) => {
if line.as_ref() != b"\r\n" { if line.as_ref() != b"\r\n" {
log::warn!( log::warn!("multipart field did not read all the data or it is malformed");
"multipart field did not read all the data or it is malformed"
);
} }
Poll::Ready(None) Poll::Ready(None)
} }

View File

@ -1,9 +1,9 @@
#![allow(clippy::uninlined_format_args)] #![allow(clippy::uninlined_format_args)]
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use std::borrow::Cow; use std::borrow::Cow;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn compare_quoters(c: &mut Criterion) { fn compare_quoters(c: &mut Criterion) {
let mut group = c.benchmark_group("Compare Quoters"); let mut group = c.benchmark_group("Compare Quoters");

View File

@ -1,10 +1,14 @@
use std::borrow::Cow; use std::borrow::Cow;
use serde::de::{self, Deserializer, Error as DeError, Visitor}; use serde::{
use serde::forward_to_deserialize_any; de::{self, Deserializer, Error as DeError, Visitor},
forward_to_deserialize_any,
};
use crate::path::{Path, PathIter}; use crate::{
use crate::{Quoter, ResourcePath}; path::{Path, PathIter},
Quoter, ResourcePath,
};
thread_local! { thread_local! {
static FULL_QUOTER: Quoter = Quoter::new(b"", b""); static FULL_QUOTER: Quoter = Quoter::new(b"", b"");
@ -486,11 +490,7 @@ impl<'de> de::VariantAccess<'de> for UnitVariant {
Err(de::value::Error::custom("not supported")) Err(de::value::Error::custom("not supported"))
} }
fn struct_variant<V>( fn struct_variant<V>(self, _: &'static [&'static str], _: V) -> Result<V::Value, Self::Error>
self,
_: &'static [&'static str],
_: V,
) -> Result<V::Value, Self::Error>
where where
V: Visitor<'de>, V: Visitor<'de>,
{ {
@ -503,9 +503,7 @@ mod tests {
use serde::{de, Deserialize}; use serde::{de, Deserialize};
use super::*; use super::*;
use crate::path::Path; use crate::{path::Path, router::Router, ResourceDef};
use crate::router::Router;
use crate::ResourceDef;
#[derive(Deserialize)] #[derive(Deserialize)]
struct MyStruct { struct MyStruct {
@ -572,13 +570,11 @@ mod tests {
assert_eq!(s.key, "name"); assert_eq!(s.key, "name");
assert_eq!(s.value, 32); assert_eq!(s.value, 32);
let s: (String, u8) = let s: (String, u8) = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap();
de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap();
assert_eq!(s.0, "name"); assert_eq!(s.0, "name");
assert_eq!(s.1, 32); assert_eq!(s.1, 32);
let res: Vec<String> = let res: Vec<String> = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap();
de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap();
assert_eq!(res[0], "name".to_owned()); assert_eq!(res[0], "name".to_owned());
assert_eq!(res[1], "32".to_owned()); assert_eq!(res[1], "32".to_owned());
} }

View File

@ -18,13 +18,14 @@ mod router;
#[cfg(feature = "http")] #[cfg(feature = "http")]
mod url; mod url;
pub use self::de::PathDeserializer;
pub use self::path::Path;
pub use self::pattern::{IntoPatterns, Patterns};
pub use self::quoter::Quoter;
pub use self::resource::ResourceDef;
pub use self::resource_path::{Resource, ResourcePath};
pub use self::router::{ResourceId, Router, RouterBuilder};
#[cfg(feature = "http")] #[cfg(feature = "http")]
pub use self::url::Url; pub use self::url::Url;
pub use self::{
de::PathDeserializer,
path::Path,
pattern::{IntoPatterns, Patterns},
quoter::Quoter,
resource::ResourceDef,
resource_path::{Resource, ResourcePath},
router::{ResourceId, Router, RouterBuilder},
};

View File

@ -1,5 +1,7 @@
use std::borrow::Cow; use std::{
use std::ops::{DerefMut, Index}; borrow::Cow,
ops::{DerefMut, Index},
};
use serde::de; use serde::de;

View File

@ -252,7 +252,7 @@ impl ResourceDef {
/// Multi-pattern resources can be constructed by providing a slice (or vec) of patterns. /// Multi-pattern resources can be constructed by providing a slice (or vec) of patterns.
/// ///
/// # Panics /// # Panics
/// Panics if path pattern is malformed. /// Panics if any path patterns are malformed.
/// ///
/// # Examples /// # Examples
/// ``` /// ```
@ -838,6 +838,7 @@ impl ResourceDef {
fn construct<T: IntoPatterns>(paths: T, is_prefix: bool) -> Self { fn construct<T: IntoPatterns>(paths: T, is_prefix: bool) -> Self {
let patterns = paths.patterns(); let patterns = paths.patterns();
let (pat_type, segments) = match &patterns { let (pat_type, segments) = match &patterns {
Patterns::Single(pattern) => ResourceDef::parse(pattern, is_prefix, false), Patterns::Single(pattern) => ResourceDef::parse(pattern, is_prefix, false),
@ -1741,9 +1742,7 @@ mod tests {
ResourceDef::new("/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}"); ResourceDef::new("/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}");
// panics // panics
ResourceDef::new( ResourceDef::new("/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}/{q}");
"/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}/{q}",
);
} }
#[test] #[test]

View File

@ -117,11 +117,7 @@ where
U: Default, U: Default,
{ {
/// Registers resource for specified path. /// Registers resource for specified path.
pub fn path( pub fn path(&mut self, path: impl IntoPatterns, val: T) -> (&mut ResourceDef, &mut T, &mut U) {
&mut self,
path: impl IntoPatterns,
val: T,
) -> (&mut ResourceDef, &mut T, &mut U) {
self.push(ResourceDef::new(path), val, U::default()) self.push(ResourceDef::new(path), val, U::default())
} }
@ -142,8 +138,10 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::path::Path; use crate::{
use crate::router::{ResourceId, Router}; path::Path,
router::{ResourceId, Router},
};
#[allow(clippy::cognitive_complexity)] #[allow(clippy::cognitive_complexity)]
#[test] #[test]

View File

@ -1,6 +1,4 @@
use crate::ResourcePath; use crate::{Quoter, ResourcePath};
use crate::Quoter;
thread_local! { thread_local! {
static DEFAULT_QUOTER: Quoter = Quoter::new(b"", b"%/+"); static DEFAULT_QUOTER: Quoter = Quoter::new(b"", b"%/+");

View File

@ -2,6 +2,7 @@
## Unreleased - 2023-xx-xx ## Unreleased - 2023-xx-xx
- Add `TestServerConfig::workers()` setter method.
- Minimum supported Rust version (MSRV) is now 1.65 due to transitive `time` dependency. - Minimum supported Rust version (MSRV) is now 1.65 due to transitive `time` dependency.
## 0.1.1 - 2023-02-26 ## 0.1.1 - 2023-02-26

View File

@ -45,8 +45,8 @@ use actix_http::{header::HeaderMap, ws, HttpService, Method, Request, Response};
pub use actix_http_test::unused_addr; pub use actix_http_test::unused_addr;
use actix_service::{map_config, IntoServiceFactory, ServiceFactory, ServiceFactoryExt as _}; use actix_service::{map_config, IntoServiceFactory, ServiceFactory, ServiceFactoryExt as _};
pub use actix_web::test::{ pub use actix_web::test::{
call_and_read_body, call_and_read_body_json, call_service, init_service, ok_service, call_and_read_body, call_and_read_body_json, call_service, init_service, ok_service, read_body,
read_body, read_body_json, status_service, TestRequest, read_body_json, status_service, TestRequest,
}; };
use actix_web::{ use actix_web::{
body::MessageBody, body::MessageBody,
@ -154,16 +154,16 @@ where
let srv_cfg = cfg.clone(); let srv_cfg = cfg.clone();
let timeout = cfg.client_request_timeout; let timeout = cfg.client_request_timeout;
let builder = Server::build().workers(1).disable_signals().system_exit(); let builder = Server::build()
.workers(cfg.workers)
.disable_signals()
.system_exit();
let srv = match srv_cfg.stream { let srv = match srv_cfg.stream {
StreamType::Tcp => match srv_cfg.tp { StreamType::Tcp => match srv_cfg.tp {
HttpVer::Http1 => builder.listen("test", tcp, move || { HttpVer::Http1 => builder.listen("test", tcp, move || {
let app_cfg = AppConfig::__priv_test_new( let app_cfg =
false, AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
local_addr.to_string(),
local_addr,
);
let fac = factory() let fac = factory()
.into_factory() .into_factory()
@ -175,11 +175,8 @@ where
.tcp() .tcp()
}), }),
HttpVer::Http2 => builder.listen("test", tcp, move || { HttpVer::Http2 => builder.listen("test", tcp, move || {
let app_cfg = AppConfig::__priv_test_new( let app_cfg =
false, AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
local_addr.to_string(),
local_addr,
);
let fac = factory() let fac = factory()
.into_factory() .into_factory()
@ -191,11 +188,8 @@ where
.tcp() .tcp()
}), }),
HttpVer::Both => builder.listen("test", tcp, move || { HttpVer::Both => builder.listen("test", tcp, move || {
let app_cfg = AppConfig::__priv_test_new( let app_cfg =
false, AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
local_addr.to_string(),
local_addr,
);
let fac = factory() let fac = factory()
.into_factory() .into_factory()
@ -210,11 +204,8 @@ where
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
StreamType::Openssl(acceptor) => match cfg.tp { StreamType::Openssl(acceptor) => match cfg.tp {
HttpVer::Http1 => builder.listen("test", tcp, move || { HttpVer::Http1 => builder.listen("test", tcp, move || {
let app_cfg = AppConfig::__priv_test_new( let app_cfg =
false, AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
local_addr.to_string(),
local_addr,
);
let fac = factory() let fac = factory()
.into_factory() .into_factory()
@ -226,11 +217,8 @@ where
.openssl(acceptor.clone()) .openssl(acceptor.clone())
}), }),
HttpVer::Http2 => builder.listen("test", tcp, move || { HttpVer::Http2 => builder.listen("test", tcp, move || {
let app_cfg = AppConfig::__priv_test_new( let app_cfg =
false, AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
local_addr.to_string(),
local_addr,
);
let fac = factory() let fac = factory()
.into_factory() .into_factory()
@ -242,11 +230,8 @@ where
.openssl(acceptor.clone()) .openssl(acceptor.clone())
}), }),
HttpVer::Both => builder.listen("test", tcp, move || { HttpVer::Both => builder.listen("test", tcp, move || {
let app_cfg = AppConfig::__priv_test_new( let app_cfg =
false, AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
local_addr.to_string(),
local_addr,
);
let fac = factory() let fac = factory()
.into_factory() .into_factory()
@ -261,11 +246,8 @@ where
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
StreamType::Rustls(config) => match cfg.tp { StreamType::Rustls(config) => match cfg.tp {
HttpVer::Http1 => builder.listen("test", tcp, move || { HttpVer::Http1 => builder.listen("test", tcp, move || {
let app_cfg = AppConfig::__priv_test_new( let app_cfg =
false, AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
local_addr.to_string(),
local_addr,
);
let fac = factory() let fac = factory()
.into_factory() .into_factory()
@ -277,11 +259,8 @@ where
.rustls(config.clone()) .rustls(config.clone())
}), }),
HttpVer::Http2 => builder.listen("test", tcp, move || { HttpVer::Http2 => builder.listen("test", tcp, move || {
let app_cfg = AppConfig::__priv_test_new( let app_cfg =
false, AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
local_addr.to_string(),
local_addr,
);
let fac = factory() let fac = factory()
.into_factory() .into_factory()
@ -293,11 +272,8 @@ where
.rustls(config.clone()) .rustls(config.clone())
}), }),
HttpVer::Both => builder.listen("test", tcp, move || { HttpVer::Both => builder.listen("test", tcp, move || {
let app_cfg = AppConfig::__priv_test_new( let app_cfg =
false, AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
local_addr.to_string(),
local_addr,
);
let fac = factory() let fac = factory()
.into_factory() .into_factory()
@ -394,6 +370,7 @@ pub struct TestServerConfig {
stream: StreamType, stream: StreamType,
client_request_timeout: Duration, client_request_timeout: Duration,
port: u16, port: u16,
workers: usize,
} }
impl Default for TestServerConfig { impl Default for TestServerConfig {
@ -410,6 +387,7 @@ impl TestServerConfig {
stream: StreamType::Tcp, stream: StreamType::Tcp,
client_request_timeout: Duration::from_secs(5), client_request_timeout: Duration::from_secs(5),
port: 0, port: 0,
workers: 1,
} }
} }
@ -452,6 +430,14 @@ impl TestServerConfig {
self.port = port; self.port = port;
self self
} }
/// Sets number of workers for the test server.
///
/// By default, the server uses 1 worker
pub fn workers(mut self, workers: usize) -> Self {
self.workers = workers;
self
}
} }
/// A basic HTTP server controller that simplifies the process of writing integration tests for /// A basic HTTP server controller that simplifies the process of writing integration tests for

View File

@ -1,11 +1,13 @@
use std::collections::VecDeque; use std::{
use std::future::Future; collections::VecDeque,
use std::pin::Pin; future::Future,
use std::task::{Context, Poll}; pin::Pin,
task::{Context, Poll},
};
use actix::dev::{AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, ToEnvelope};
use actix::fut::ActorFuture;
use actix::{ use actix::{
dev::{AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, ToEnvelope},
fut::ActorFuture,
Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, Message, SpawnHandle, Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, Message, SpawnHandle,
}; };
use actix_web::error::Error; use actix_web::error::Error;
@ -247,9 +249,11 @@ mod tests {
use std::time::Duration; use std::time::Duration;
use actix::Actor; use actix::Actor;
use actix_web::http::StatusCode; use actix_web::{
use actix_web::test::{call_service, init_service, read_body, TestRequest}; http::StatusCode,
use actix_web::{web, App, HttpResponse}; test::{call_service, init_service, read_body, TestRequest},
web, App, HttpResponse,
};
use bytes::Bytes; use bytes::Bytes;
use super::*; use super::*;

View File

@ -66,17 +66,14 @@ use std::{
use actix::{ use actix::{
dev::{ dev::{
AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, StreamHandler, AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, StreamHandler, ToEnvelope,
ToEnvelope,
}, },
fut::ActorFuture, fut::ActorFuture,
Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, Message as ActixMessage, Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, Message as ActixMessage,
SpawnHandle, SpawnHandle,
}; };
use actix_http::ws::{hash_key, Codec}; use actix_http::ws::{hash_key, Codec};
pub use actix_http::ws::{ pub use actix_http::ws::{CloseCode, CloseReason, Frame, HandshakeError, Message, ProtocolError};
CloseCode, CloseReason, Frame, HandshakeError, Message, ProtocolError,
};
use actix_web::{ use actix_web::{
error::{Error, PayloadError}, error::{Error, PayloadError},
http::{ http::{
@ -426,16 +423,16 @@ pub fn handshake_with_protocols(
}; };
// check requested protocols // check requested protocols
let protocol = let protocol = req
req.headers() .headers()
.get(&header::SEC_WEBSOCKET_PROTOCOL) .get(&header::SEC_WEBSOCKET_PROTOCOL)
.and_then(|req_protocols| { .and_then(|req_protocols| {
let req_protocols = req_protocols.to_str().ok()?; let req_protocols = req_protocols.to_str().ok()?;
req_protocols req_protocols
.split(',') .split(',')
.map(|req_p| req_p.trim()) .map(|req_p| req_p.trim())
.find(|req_p| protocols.iter().any(|p| p == req_p)) .find(|req_p| protocols.iter().any(|p| p == req_p))
}); });
let mut response = HttpResponse::build(StatusCode::SWITCHING_PROTOCOLS) let mut response = HttpResponse::build(StatusCode::SWITCHING_PROTOCOLS)
.upgrade("websocket") .upgrade("websocket")

View File

@ -2,6 +2,7 @@
## Unreleased - 2023-xx-xx ## Unreleased - 2023-xx-xx
- Update `syn` dependency to `2`.
- Minimum supported Rust version (MSRV) is now 1.65 due to transitive `time` dependency. - Minimum supported Rust version (MSRV) is now 1.65 due to transitive `time` dependency.
## 4.2.0 - 2023-02-26 ## 4.2.0 - 2023-02-26

View File

@ -18,10 +18,10 @@ proc-macro = true
actix-router = "0.5" actix-router = "0.5"
proc-macro2 = "1" proc-macro2 = "1"
quote = "1" quote = "1"
syn = { version = "1", features = ["full", "extra-traits"] } syn = { version = "2", features = ["full", "extra-traits"] }
[dev-dependencies] [dev-dependencies]
actix-macros = "0.2.3" actix-macros = "0.2.4"
actix-rt = "2.2" actix-rt = "2.2"
actix-test = "0.1" actix-test = "0.1"
actix-utils = "3" actix-utils = "3"

View File

@ -153,37 +153,37 @@ pub fn routes(_: TokenStream, input: TokenStream) -> TokenStream {
macro_rules! method_macro { macro_rules! method_macro {
($variant:ident, $method:ident) => { ($variant:ident, $method:ident) => {
#[doc = concat!("Creates route handler with `actix_web::guard::", stringify!($variant), "`.")] #[doc = concat!("Creates route handler with `actix_web::guard::", stringify!($variant), "`.")]
/// ///
/// # Syntax /// # Syntax
/// ```plain /// ```plain
#[doc = concat!("#[", stringify!($method), r#"("path"[, attributes])]"#)] #[doc = concat!("#[", stringify!($method), r#"("path"[, attributes])]"#)]
/// ``` /// ```
/// ///
/// # Attributes /// # Attributes
/// - `"path"`: Raw literal string with path for which to register handler. /// - `"path"`: Raw literal string with path for which to register handler.
/// - `name = "resource_name"`: Specifies resource name for the handler. If not set, the function /// - `name = "resource_name"`: Specifies resource name for the handler. If not set, the
/// name of handler is used. /// function name of handler is used.
/// - `guard = "function_name"`: Registers function as guard using `actix_web::guard::fn_guard`. /// - `guard = "function_name"`: Registers function as guard using `actix_web::guard::fn_guard`.
/// - `wrap = "Middleware"`: Registers a resource middleware. /// - `wrap = "Middleware"`: Registers a resource middleware.
/// ///
/// # Notes /// # Notes
/// Function name can be specified as any expression that is going to be accessible to the /// Function name can be specified as any expression that is going to be accessible to the
/// generate code, e.g `my_guard` or `my_module::my_guard`. /// generate code, e.g `my_guard` or `my_module::my_guard`.
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_web::HttpResponse; /// # use actix_web::HttpResponse;
#[doc = concat!("# use actix_web_codegen::", stringify!($method), ";")] #[doc = concat!("# use actix_web_codegen::", stringify!($method), ";")]
#[doc = concat!("#[", stringify!($method), r#"("/")]"#)] #[doc = concat!("#[", stringify!($method), r#"("/")]"#)]
/// async fn example() -> HttpResponse { /// async fn example() -> HttpResponse {
/// HttpResponse::Ok().finish() /// HttpResponse::Ok().finish()
/// } /// }
/// ``` /// ```
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn $method(args: TokenStream, input: TokenStream) -> TokenStream { pub fn $method(args: TokenStream, input: TokenStream) -> TokenStream {
route::with_method(Some(route::MethodType::$variant), args, input) route::with_method(Some(route::MethodType::$variant), args, input)
} }
}; };
} }

View File

@ -4,7 +4,54 @@ use actix_router::ResourceDef;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2}; use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens, TokenStreamExt}; use quote::{quote, ToTokens, TokenStreamExt};
use syn::{parse_macro_input, AttributeArgs, Ident, LitStr, Meta, NestedMeta, Path}; use syn::{punctuated::Punctuated, Ident, LitStr, Path, Token};
#[derive(Debug)]
pub struct RouteArgs {
path: syn::LitStr,
options: Punctuated<syn::MetaNameValue, Token![,]>,
}
impl syn::parse::Parse for RouteArgs {
fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
// path to match: "/foo"
let path = input.parse::<syn::LitStr>().map_err(|mut err| {
err.combine(syn::Error::new(
err.span(),
r#"invalid service definition, expected #[<method>("<path>")]"#,
));
err
})?;
// verify that path pattern is valid
let _ = ResourceDef::new(path.value());
// if there's no comma, assume that no options are provided
if !input.peek(Token![,]) {
return Ok(Self {
path,
options: Punctuated::new(),
});
}
// advance past comma separator
input.parse::<Token![,]>()?;
// if next char is a literal, assume that it is a string and show multi-path error
if input.cursor().literal().is_some() {
return Err(syn::Error::new(
Span::call_site(),
r#"Multiple paths specified! There should be only one."#,
));
}
// zero or more options: name = "foo"
let options = input.parse_terminated(syn::MetaNameValue::parse, Token![,])?;
Ok(Self { path, options })
}
}
macro_rules! standard_method_type { macro_rules! standard_method_type {
( (
@ -182,111 +229,90 @@ struct Args {
} }
impl Args { impl Args {
fn new(args: AttributeArgs, method: Option<MethodType>) -> syn::Result<Self> { fn new(args: RouteArgs, method: Option<MethodType>) -> syn::Result<Self> {
let mut path = None;
let mut resource_name = None; let mut resource_name = None;
let mut guards = Vec::new(); let mut guards = Vec::new();
let mut wrappers = Vec::new(); let mut wrappers = Vec::new();
let mut methods = HashSet::new(); let mut methods = HashSet::new();
if args.is_empty() {
return Err(syn::Error::new(
Span::call_site(),
format!(
r#"invalid service definition, expected #[{}("<path>")]"#,
method
.map_or("route", |it| it.as_str())
.to_ascii_lowercase()
),
));
}
let is_route_macro = method.is_none(); let is_route_macro = method.is_none();
if let Some(method) = method { if let Some(method) = method {
methods.insert(MethodTypeExt::Standard(method)); methods.insert(MethodTypeExt::Standard(method));
} }
for arg in args { for nv in args.options {
match arg { if nv.path.is_ident("name") {
NestedMeta::Lit(syn::Lit::Str(lit)) => match path { if let syn::Expr::Lit(syn::ExprLit {
None => { lit: syn::Lit::Str(lit),
let _ = ResourceDef::new(lit.value()); ..
path = Some(lit); }) = nv.value
} {
_ => { resource_name = Some(lit);
return Err(syn::Error::new_spanned( } else {
lit, return Err(syn::Error::new_spanned(
"Multiple paths specified! Should be only one!", nv.value,
)); "Attribute name expects literal string!",
} ));
}, }
} else if nv.path.is_ident("guard") {
NestedMeta::Meta(syn::Meta::NameValue(nv)) => { if let syn::Expr::Lit(syn::ExprLit {
if nv.path.is_ident("name") { lit: syn::Lit::Str(lit),
if let syn::Lit::Str(lit) = nv.lit { ..
resource_name = Some(lit); }) = nv.value
} else { {
return Err(syn::Error::new_spanned( guards.push(lit.parse::<Path>()?);
nv.lit, } else {
"Attribute name expects literal string!", return Err(syn::Error::new_spanned(
)); nv.value,
} "Attribute guard expects literal string!",
} else if nv.path.is_ident("guard") { ));
if let syn::Lit::Str(lit) = nv.lit { }
guards.push(lit.parse::<Path>()?); } else if nv.path.is_ident("wrap") {
} else { if let syn::Expr::Lit(syn::ExprLit {
return Err(syn::Error::new_spanned( lit: syn::Lit::Str(lit),
nv.lit, ..
"Attribute guard expects literal string!", }) = nv.value
)); {
} wrappers.push(lit.parse()?);
} else if nv.path.is_ident("wrap") { } else {
if let syn::Lit::Str(lit) = nv.lit { return Err(syn::Error::new_spanned(
wrappers.push(lit.parse()?); nv.value,
} else { "Attribute wrap expects type",
return Err(syn::Error::new_spanned( ));
nv.lit, }
"Attribute wrap expects type", } else if nv.path.is_ident("method") {
)); if !is_route_macro {
} return Err(syn::Error::new_spanned(
} else if nv.path.is_ident("method") {
if !is_route_macro {
return Err(syn::Error::new_spanned(
&nv, &nv,
"HTTP method forbidden here. To handle multiple methods, use `route` instead", "HTTP method forbidden here. To handle multiple methods, use `route` instead",
)); ));
} else if let syn::Lit::Str(ref lit) = nv.lit { } else if let syn::Expr::Lit(syn::ExprLit {
if !methods.insert(MethodTypeExt::try_from(lit)?) { lit: syn::Lit::Str(lit),
return Err(syn::Error::new_spanned( ..
&nv.lit, }) = nv.value.clone()
format!( {
"HTTP method defined more than once: `{}`", if !methods.insert(MethodTypeExt::try_from(&lit)?) {
lit.value()
),
));
}
} else {
return Err(syn::Error::new_spanned(
nv.lit,
"Attribute method expects literal string!",
));
}
} else {
return Err(syn::Error::new_spanned( return Err(syn::Error::new_spanned(
nv.path, nv.value,
"Unknown attribute key is specified. Allowed: guard, method and wrap", format!("HTTP method defined more than once: `{}`", lit.value()),
)); ));
} }
} else {
return Err(syn::Error::new_spanned(
nv.value,
"Attribute method expects literal string!",
));
} }
} else {
arg => { return Err(syn::Error::new_spanned(
return Err(syn::Error::new_spanned(arg, "Unknown attribute.")); nv.path,
} "Unknown attribute key is specified. Allowed: guard, method and wrap",
));
} }
} }
Ok(Args { Ok(Args {
path: path.unwrap(), path: args.path,
resource_name, resource_name,
guards, guards,
wrappers, wrappers,
@ -312,11 +338,7 @@ pub struct Route {
} }
impl Route { impl Route {
pub fn new( pub fn new(args: RouteArgs, ast: syn::ItemFn, method: Option<MethodType>) -> syn::Result<Self> {
args: AttributeArgs,
ast: syn::ItemFn,
method: Option<MethodType>,
) -> syn::Result<Self> {
let name = ast.sig.ident.clone(); let name = ast.sig.ident.clone();
// Try and pull out the doc comments so that we can reapply them to the generated struct. // Try and pull out the doc comments so that we can reapply them to the generated struct.
@ -324,7 +346,7 @@ impl Route {
let doc_attributes = ast let doc_attributes = ast
.attrs .attrs
.iter() .iter()
.filter(|attr| attr.path.is_ident("doc")) .filter(|attr| attr.path().is_ident("doc"))
.cloned() .cloned()
.collect(); .collect();
@ -360,7 +382,7 @@ impl Route {
let doc_attributes = ast let doc_attributes = ast
.attrs .attrs
.iter() .iter()
.filter(|attr| attr.path.is_ident("doc")) .filter(|attr| attr.path().is_ident("doc"))
.cloned() .cloned()
.collect(); .collect();
@ -455,7 +477,11 @@ pub(crate) fn with_method(
args: TokenStream, args: TokenStream,
input: TokenStream, input: TokenStream,
) -> TokenStream { ) -> TokenStream {
let args = parse_macro_input!(args as syn::AttributeArgs); let args = match syn::parse(args) {
Ok(args) => args,
// on parse error, make IDEs happy; see fn docs
Err(err) => return input_and_compile_error(input, err),
};
let ast = match syn::parse::<syn::ItemFn>(input.clone()) { let ast = match syn::parse::<syn::ItemFn>(input.clone()) {
Ok(ast) => ast, Ok(ast) => ast,
@ -480,7 +506,7 @@ pub(crate) fn with_methods(input: TokenStream) -> TokenStream {
let (methods, others) = ast let (methods, others) = ast
.attrs .attrs
.into_iter() .into_iter()
.map(|attr| match MethodType::from_path(&attr.path) { .map(|attr| match MethodType::from_path(attr.path()) {
Ok(method) => Ok((method, attr)), Ok(method) => Ok((method, attr)),
Err(_) => Err(attr), Err(_) => Err(attr),
}) })
@ -488,31 +514,27 @@ pub(crate) fn with_methods(input: TokenStream) -> TokenStream {
ast.attrs = others.into_iter().map(Result::unwrap_err).collect(); ast.attrs = others.into_iter().map(Result::unwrap_err).collect();
let methods = let methods = match methods
match methods .into_iter()
.into_iter() .map(Result::unwrap)
.map(Result::unwrap) .map(|(method, attr)| {
.map(|(method, attr)| { attr.parse_args()
attr.parse_meta().and_then(|args| { .and_then(|args| Args::new(args, Some(method)))
if let Meta::List(args) = args { })
Args::new(args.nested.into_iter().collect(), Some(method)) .collect::<Result<Vec<_>, _>>()
} else { {
Err(syn::Error::new_spanned(attr, "Invalid input for macro")) Ok(methods) if methods.is_empty() => {
} return input_and_compile_error(
})
})
.collect::<Result<Vec<_>, _>>()
{
Ok(methods) if methods.is_empty() => return input_and_compile_error(
input, input,
syn::Error::new( syn::Error::new(
Span::call_site(), Span::call_site(),
"The #[routes] macro requires at least one `#[<method>(..)]` attribute.", "The #[routes] macro requires at least one `#[<method>(..)]` attribute.",
), ),
), )
Ok(methods) => methods, }
Err(err) => return input_and_compile_error(input, err), Ok(methods) => methods,
}; Err(err) => return input_and_compile_error(input, err),
};
match Route::multiple(methods, ast) { match Route::multiple(methods, ast) {
Ok(route) => route.into_token_stream().into(), Ok(route) => route.into_token_stream().into(),

View File

@ -1,4 +1,4 @@
error: invalid service definition, expected #[get("<path>")] error: unexpected end of input, expected string literal
--> tests/trybuild/routes-missing-args-fail.rs:4:1 --> tests/trybuild/routes-missing-args-fail.rs:4:1
| |
4 | #[get] 4 | #[get]
@ -6,11 +6,19 @@ error: invalid service definition, expected #[get("<path>")]
| |
= note: this error originates in the attribute macro `get` (in Nightly builds, run with -Z macro-backtrace for more info) = note: this error originates in the attribute macro `get` (in Nightly builds, run with -Z macro-backtrace for more info)
error: Invalid input for macro error: invalid service definition, expected #[<method>("<path>")]
--> tests/trybuild/routes-missing-args-fail.rs:4:1 --> tests/trybuild/routes-missing-args-fail.rs:4:1
| |
4 | #[get] 4 | #[get]
| ^^^^^^ | ^^^^^^
|
= note: this error originates in the attribute macro `get` (in Nightly builds, run with -Z macro-backtrace for more info)
error: expected attribute arguments in parentheses: #[get(...)]
--> tests/trybuild/routes-missing-args-fail.rs:4:3
|
4 | #[get]
| ^^^
error[E0277]: the trait bound `fn() -> impl std::future::Future<Output = String> {index}: HttpServiceFactory` is not satisfied error[E0277]: the trait bound `fn() -> impl std::future::Future<Output = String> {index}: HttpServiceFactory` is not satisfied
--> tests/trybuild/routes-missing-args-fail.rs:13:55 --> tests/trybuild/routes-missing-args-fail.rs:13:55

View File

@ -1,26 +1,42 @@
error: Unknown attribute. error: expected `=`
--> $DIR/simple-fail.rs:3:15 --> $DIR/simple-fail.rs:3:1
| |
3 | #[get("/one", other)] 3 | #[get("/one", other)]
| ^^^^^ | ^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in the attribute macro `get` (in Nightly builds, run with -Z macro-backtrace for more info)
error: expected identifier or literal error: expected string literal
--> $DIR/simple-fail.rs:8:8 --> $DIR/simple-fail.rs:8:8
| |
8 | #[post(/two)] 8 | #[post(/two)]
| ^ | ^
error: Unknown attribute. error: invalid service definition, expected #[<method>("<path>")]
--> $DIR/simple-fail.rs:8:8
|
8 | #[post(/two)]
| ^
error: expected string literal
--> $DIR/simple-fail.rs:15:9 --> $DIR/simple-fail.rs:15:9
| |
15 | #[patch(PATCH_PATH)] 15 | #[patch(PATCH_PATH)]
| ^^^^^^^^^^ | ^^^^^^^^^^
error: Multiple paths specified! Should be only one! error: invalid service definition, expected #[<method>("<path>")]
--> $DIR/simple-fail.rs:20:19 --> $DIR/simple-fail.rs:15:9
|
15 | #[patch(PATCH_PATH)]
| ^^^^^^^^^^
error: Multiple paths specified! There should be only one.
--> $DIR/simple-fail.rs:20:1
| |
20 | #[delete("/four", "/five")] 20 | #[delete("/four", "/five")]
| ^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in the attribute macro `delete` (in Nightly builds, run with -Z macro-backtrace for more info)
error: HTTP method forbidden here. To handle multiple methods, use `route` instead error: HTTP method forbidden here. To handle multiple methods, use `route` instead
--> $DIR/simple-fail.rs:25:19 --> $DIR/simple-fail.rs:25:19

View File

@ -4,13 +4,15 @@
### Added ### Added
- Add `HttpServer::{bind,listen}_auto_h2c()` method. - Add `HttpServer::{bind, listen}_auto_h2c()` method behind new `http2` crate feature.
- Add `Resource::{get, post, etc...}` methods for more concisely adding routes that don't need additional guards. - Add `Resource::{get, post, etc...}` methods for more concisely adding routes that don't need additional guards.
- Add several missing convenience methods on `HttpResponse` for their respective status codes. - Add several missing convenience methods on `HttpResponse` for their respective status codes.
### Changed ### Changed
- Handler functions can now receive up to 16 extractor parameters. - Handler functions can now receive up to 16 extractor parameters.
- The `Compress` middleware no longer compresses image or video content.
- Hide sensitive header values in `HttpRequest`'s `Debug` output.
- Minimum supported Rust version (MSRV) is now 1.65 due to transitive `time` dependency. - Minimum supported Rust version (MSRV) is now 1.65 due to transitive `time` dependency.
## 4.3.1 - 2023-02-26 ## 4.3.1 - 2023-02-26

View File

@ -28,7 +28,7 @@ name = "actix_web"
path = "src/lib.rs" path = "src/lib.rs"
[features] [features]
default = ["macros", "compress-brotli", "compress-gzip", "compress-zstd", "cookies"] default = ["macros", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", "http2"]
# Brotli algorithm content-encoding support # Brotli algorithm content-encoding support
compress-brotli = ["actix-http/compress-brotli", "__compress"] compress-brotli = ["actix-http/compress-brotli", "__compress"]
@ -46,11 +46,13 @@ cookies = ["cookie"]
# Secure & signed cookies # Secure & signed cookies
secure-cookies = ["cookies", "cookie/secure"] secure-cookies = ["cookies", "cookie/secure"]
http2 = ["actix-http/http2"]
# TLS via OpenSSL # TLS via OpenSSL
openssl = ["actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"] openssl = ["http2", "actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"]
# TLS via Rustls # TLS via Rustls
rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"] rustls = ["http2", "actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"]
# Internal (PRIVATE!) features used to aid testing and checking feature status. # Internal (PRIVATE!) features used to aid testing and checking feature status.
# Don't rely on these whatsoever. They may disappear at anytime. # Don't rely on these whatsoever. They may disappear at anytime.
@ -68,7 +70,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.3", features = ["http2", "ws"] } actix-http = { version = "3.3", features = ["ws"] }
actix-router = "0.5" actix-router = "0.5"
actix-web-codegen = { version = "4.2", optional = true } actix-web-codegen = { version = "4.2", optional = true }

View File

@ -99,8 +99,7 @@ fn responder(c: &mut Criterion) {
let req = TestRequest::default().to_http_request(); let req = TestRequest::default().to_http_request();
c.bench_function("responder", move |b| { c.bench_function("responder", move |b| {
b.iter_custom(|_| { b.iter_custom(|_| {
let responders = let responders = (0..100_000).map(|_| StringResponder(String::from("Hello World!!")));
(0..100_000).map(|_| StringResponder(String::from("Hello World!!")));
let start = Instant::now(); let start = Instant::now();
let _res = rt.block_on(async { let _res = rt.block_on(async {

View File

@ -1,11 +1,12 @@
use actix_service::Service; use std::{cell::RefCell, rc::Rc};
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::{web, App, Error, HttpResponse};
use criterion::{criterion_main, Criterion};
use std::cell::RefCell;
use std::rc::Rc;
use actix_web::test::{init_service, ok_service, TestRequest}; use actix_service::Service;
use actix_web::{
dev::{ServiceRequest, ServiceResponse},
test::{init_service, ok_service, TestRequest},
web, App, Error, HttpResponse,
};
use criterion::{criterion_main, Criterion};
/// Criterion Benchmark for async Service /// Criterion Benchmark for async Service
/// Should be used from within criterion group: /// Should be used from within criterion group:

View File

@ -7,8 +7,7 @@
use std::{any::Any, io, net::SocketAddr}; use std::{any::Any, io, net::SocketAddr};
use actix_web::{ use actix_web::{
dev::Extensions, rt::net::TcpStream, web, App, HttpRequest, HttpResponse, HttpServer, dev::Extensions, rt::net::TcpStream, web, App, HttpRequest, HttpResponse, HttpServer, Responder,
Responder,
}; };
#[allow(dead_code)] #[allow(dead_code)]
@ -24,9 +23,7 @@ async fn route_whoami(req: HttpRequest) -> impl Responder {
Some(info) => HttpResponse::Ok().body(format!( Some(info) => HttpResponse::Ok().body(format!(
"Here is some info about your connection:\n\n{info:#?}", "Here is some info about your connection:\n\n{info:#?}",
)), )),
None => { None => HttpResponse::InternalServerError().body("Missing expected request extension data"),
HttpResponse::InternalServerError().body("Missing expected request extension data")
}
} }
} }

View File

@ -264,12 +264,8 @@ where
pub fn default_service<F, U>(mut self, svc: F) -> Self pub fn default_service<F, U>(mut self, svc: F) -> Self
where where
F: IntoServiceFactory<U, ServiceRequest>, F: IntoServiceFactory<U, ServiceRequest>,
U: ServiceFactory< U: ServiceFactory<ServiceRequest, Config = (), Response = ServiceResponse, Error = Error>
ServiceRequest, + 'static,
Config = (),
Response = ServiceResponse,
Error = Error,
> + 'static,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
let svc = svc let svc = svc
@ -323,16 +319,7 @@ where
/// Middleware can be applied similarly to individual `Scope`s and `Resource`s. /// Middleware can be applied similarly to individual `Scope`s and `Resource`s.
/// See [`Scope::wrap`](crate::Scope::wrap) and [`Resource::wrap`]. /// See [`Scope::wrap`](crate::Scope::wrap) and [`Resource::wrap`].
/// ///
/// # Middleware Order /// For more info on middleware take a look at the [`middleware` module][crate::middleware].
/// Notice that the keyword for registering middleware is `wrap`. As you register middleware
/// using `wrap` in the App builder, imagine wrapping layers around an inner App. The first
/// middleware layer exposed to a Request is the outermost layer (i.e., the *last* registered in
/// the builder chain). Consequently, the *first* middleware registered in the builder chain is
/// the *last* to start executing during request processing.
///
/// Ordering is less obvious when wrapped services also have middleware applied. In this case,
/// middlewares are run in reverse order for `App` _and then_ in reverse order for the
/// wrapped service.
/// ///
/// # Examples /// # Examples
/// ``` /// ```

View File

@ -348,13 +348,17 @@ impl ServiceFactory<ServiceRequest> for AppEntry {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{
use std::sync::Arc; atomic::{AtomicBool, Ordering},
Arc,
};
use actix_service::Service; use actix_service::Service;
use crate::test::{init_service, TestRequest}; use crate::{
use crate::{web, App, HttpResponse}; test::{init_service, TestRequest},
web, App, HttpResponse,
};
struct DropData(Arc<AtomicBool>); struct DropData(Arc<AtomicBool>);

View File

@ -232,12 +232,8 @@ impl ServiceConfig {
pub fn default_service<F, U>(&mut self, f: F) -> &mut Self pub fn default_service<F, U>(&mut self, f: F) -> &mut Self
where where
F: IntoServiceFactory<U, ServiceRequest>, F: IntoServiceFactory<U, ServiceRequest>,
U: ServiceFactory< U: ServiceFactory<ServiceRequest, Config = (), Response = ServiceResponse, Error = Error>
ServiceRequest, + 'static,
Config = (),
Response = ServiceResponse,
Error = Error,
> + 'static,
U::InitError: std::fmt::Debug, U::InitError: std::fmt::Debug,
{ {
let svc = f let svc = f
@ -308,9 +304,11 @@ mod tests {
use bytes::Bytes; use bytes::Bytes;
use super::*; use super::*;
use crate::http::{Method, StatusCode}; use crate::{
use crate::test::{assert_body_eq, call_service, init_service, read_body, TestRequest}; http::{Method, StatusCode},
use crate::{web, App, HttpRequest, HttpResponse}; test::{assert_body_eq, call_service, init_service, read_body, TestRequest},
web, App, HttpRequest, HttpResponse,
};
// allow deprecated `ServiceConfig::data` // allow deprecated `ServiceConfig::data`
#[allow(deprecated)] #[allow(deprecated)]

View File

@ -186,12 +186,14 @@ mod tests {
#[allow(deprecated)] #[allow(deprecated)]
#[actix_rt::test] #[actix_rt::test]
async fn test_data_extractor() { async fn test_data_extractor() {
let srv = init_service(App::new().data("TEST".to_string()).service( let srv = init_service(
web::resource("/").to(|data: web::Data<String>| { App::new()
assert_eq!(data.to_lowercase(), "test"); .data("TEST".to_string())
HttpResponse::Ok() .service(web::resource("/").to(|data: web::Data<String>| {
}), assert_eq!(data.to_lowercase(), "test");
)) HttpResponse::Ok()
})),
)
.await; .await;
let req = TestRequest::default().to_request(); let req = TestRequest::default().to_request();
@ -286,16 +288,17 @@ mod tests {
#[allow(deprecated)] #[allow(deprecated)]
#[actix_rt::test] #[actix_rt::test]
async fn test_override_data() { async fn test_override_data() {
let srv = let srv = init_service(
init_service(App::new().data(1usize).service( App::new()
web::resource("/").data(10usize).route(web::get().to( .data(1usize)
.service(web::resource("/").data(10usize).route(web::get().to(
|data: web::Data<usize>| { |data: web::Data<usize>| {
assert_eq!(**data, 10); assert_eq!(**data, 10);
HttpResponse::Ok() HttpResponse::Ok()
}, },
)), ))),
)) )
.await; .await;
let req = TestRequest::default().to_request(); let req = TestRequest::default().to_request();
let resp = srv.call(req).await.unwrap(); let resp = srv.call(req).await.unwrap();

View File

@ -7,26 +7,25 @@
//! - [`ConnectionInfo`]: Connection information //! - [`ConnectionInfo`]: Connection information
//! - [`PeerAddr`]: Connection information //! - [`PeerAddr`]: Connection information
#[cfg(feature = "__compress")]
pub use actix_http::encoding::Decoder as Decompress;
pub use actix_http::{Extensions, Payload, RequestHead, Response, ResponseHead}; pub use actix_http::{Extensions, Payload, RequestHead, Response, ResponseHead};
use actix_router::Patterns;
pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; pub use actix_router::{Path, ResourceDef, ResourcePath, Url};
pub use actix_server::{Server, ServerHandle}; pub use actix_server::{Server, ServerHandle};
pub use actix_service::{ pub use actix_service::{
always_ready, fn_factory, fn_service, forward_ready, Service, ServiceFactory, Transform, always_ready, fn_factory, fn_service, forward_ready, Service, ServiceFactory, Transform,
}; };
#[cfg(feature = "__compress")]
pub use actix_http::encoding::Decoder as Decompress;
pub use crate::config::{AppConfig, AppService};
#[doc(hidden)] #[doc(hidden)]
pub use crate::handler::Handler; pub use crate::handler::Handler;
pub use crate::info::{ConnectionInfo, PeerAddr}; pub use crate::{
pub use crate::rmap::ResourceMap; config::{AppConfig, AppService},
pub use crate::service::{HttpServiceFactory, ServiceRequest, ServiceResponse, WebService}; info::{ConnectionInfo, PeerAddr},
rmap::ResourceMap,
pub use crate::types::{JsonBody, Readlines, UrlEncoded}; service::{HttpServiceFactory, ServiceRequest, ServiceResponse, WebService},
types::{JsonBody, Readlines, UrlEncoded},
use actix_router::Patterns; };
pub(crate) fn ensure_leading_slash(mut patterns: Patterns) -> Patterns { pub(crate) fn ensure_leading_slash(mut patterns: Patterns) -> Patterns {
match &mut patterns { match &mut patterns {

View File

@ -42,8 +42,7 @@ macro_rules! downcast_dyn {
/// Downcasts generic body to a specific type. /// Downcasts generic body to a specific type.
#[allow(dead_code)] #[allow(dead_code)]
pub fn downcast_ref<T: $name + 'static>(&self) -> Option<&T> { pub fn downcast_ref<T: $name + 'static>(&self) -> Option<&T> {
if self.__private_get_type_id__(PrivateHelper(())).0 if self.__private_get_type_id__(PrivateHelper(())).0 == std::any::TypeId::of::<T>()
== std::any::TypeId::of::<T>()
{ {
// SAFETY: external crates cannot override the default // SAFETY: external crates cannot override the default
// implementation of `__private_get_type_id__`, since // implementation of `__private_get_type_id__`, since
@ -59,8 +58,7 @@ macro_rules! downcast_dyn {
/// Downcasts a generic body to a mutable specific type. /// Downcasts a generic body to a mutable specific type.
#[allow(dead_code)] #[allow(dead_code)]
pub fn downcast_mut<T: $name + 'static>(&mut self) -> Option<&mut T> { pub fn downcast_mut<T: $name + 'static>(&mut self) -> Option<&mut T> {
if self.__private_get_type_id__(PrivateHelper(())).0 if self.__private_get_type_id__(PrivateHelper(())).0 == std::any::TypeId::of::<T>()
== std::any::TypeId::of::<T>()
{ {
// SAFETY: external crates cannot override the default // SAFETY: external crates cannot override the default
// implementation of `__private_get_type_id__`, since // implementation of `__private_get_type_id__`, since
@ -76,7 +74,8 @@ macro_rules! downcast_dyn {
}; };
} }
pub(crate) use {downcast_dyn, downcast_get_type_id}; pub(crate) use downcast_dyn;
pub(crate) use downcast_get_type_id;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {

View File

@ -5,14 +5,10 @@
// expanded manually. // expanded manually.
// //
// See <https://github.com/rust-lang/rust/issues/83375> // See <https://github.com/rust-lang/rust/issues/83375>
pub use actix_http::error::{ pub use actix_http::error::{ContentTypeError, DispatchError, HttpError, ParseError, PayloadError};
ContentTypeError, DispatchError, HttpError, ParseError, PayloadError,
};
use derive_more::{Display, Error, From}; use derive_more::{Display, Error, From};
use serde_json::error::Error as JsonError; use serde_json::error::Error as JsonError;
use serde_urlencoded::de::Error as FormDeError; use serde_urlencoded::{de::Error as FormDeError, ser::Error as FormError};
use serde_urlencoded::ser::Error as FormError;
use url::ParseError as UrlParseError; use url::ParseError as UrlParseError;
use crate::http::StatusCode; use crate::http::StatusCode;
@ -23,10 +19,8 @@ mod internal;
mod macros; mod macros;
mod response_error; mod response_error;
pub use self::error::Error; pub(crate) use self::macros::{downcast_dyn, downcast_get_type_id};
pub use self::internal::*; pub use self::{error::Error, internal::*, response_error::ResponseError};
pub use self::response_error::ResponseError;
pub(crate) use macros::{downcast_dyn, downcast_get_type_id};
/// A convenience [`Result`](std::result::Result) for Actix Web operations. /// A convenience [`Result`](std::result::Result) for Actix Web operations.
/// ///

View File

@ -429,8 +429,10 @@ mod tests {
use serde::Deserialize; use serde::Deserialize;
use super::*; use super::*;
use crate::test::TestRequest; use crate::{
use crate::types::{Form, FormConfig}; test::TestRequest,
types::{Form, FormConfig},
};
#[derive(Deserialize, Debug, PartialEq)] #[derive(Deserialize, Debug, PartialEq)]
struct Info { struct Info {

View File

@ -61,8 +61,10 @@ use crate::{http::header::Header, service::ServiceRequest, HttpMessage as _};
mod acceptable; mod acceptable;
mod host; mod host;
pub use self::acceptable::Acceptable; pub use self::{
pub use self::host::{Host, HostGuard}; acceptable::Acceptable,
host::{Host, HostGuard},
};
/// Provides access to request parts that are useful during routing. /// Provides access to request parts that are useful during routing.
#[derive(Debug)] #[derive(Debug)]

View File

@ -94,10 +94,7 @@ impl AcceptEncoding {
/// includes the server's supported encodings in the body plus a [`Vary`] header. /// includes the server's supported encodings in the body plus a [`Vary`] header.
/// ///
/// [`Vary`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary /// [`Vary`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary
pub fn negotiate<'a>( pub fn negotiate<'a>(&self, supported: impl Iterator<Item = &'a Encoding>) -> Option<Encoding> {
&self,
supported: impl Iterator<Item = &'a Encoding>,
) -> Option<Encoding> {
// 1. If no Accept-Encoding field is in the request, any content-coding is considered // 1. If no Accept-Encoding field is in the request, any content-coding is considered
// acceptable by the user agent. // acceptable by the user agent.
@ -375,9 +372,7 @@ mod tests {
Some(Encoding::deflate()) Some(Encoding::deflate())
); );
assert_eq!( assert_eq!(
test.negotiate( test.negotiate([Encoding::gzip(), Encoding::deflate(), Encoding::identity()].iter()),
[Encoding::gzip(), Encoding::deflate(), Encoding::identity()].iter()
),
Some(Encoding::gzip()) Some(Encoding::gzip())
); );
assert_eq!( assert_eq!(

View File

@ -592,9 +592,8 @@ mod tests {
fn test_from_raw_basic() { fn test_from_raw_basic() {
assert!(ContentDisposition::from_raw(&HeaderValue::from_static("")).is_err()); assert!(ContentDisposition::from_raw(&HeaderValue::from_static("")).is_err());
let a = HeaderValue::from_static( let a =
"form-data; dummy=3; name=upload; filename=\"sample.png\"", HeaderValue::from_static("form-data; dummy=3; name=upload; filename=\"sample.png\"");
);
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition { let b = ContentDisposition {
disposition: DispositionType::FormData, disposition: DispositionType::FormData,
@ -648,8 +647,8 @@ mod tests {
charset: Charset::Ext(String::from("UTF-8")), charset: Charset::Ext(String::from("UTF-8")),
language_tag: None, language_tag: None,
value: vec![ value: vec![
0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20, b'r', 0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20, b'r', b'a',
b'a', b't', b'e', b's', b't', b'e', b's',
], ],
})], })],
}; };
@ -665,8 +664,8 @@ mod tests {
charset: Charset::Ext(String::from("UTF-8")), charset: Charset::Ext(String::from("UTF-8")),
language_tag: None, language_tag: None,
value: vec![ value: vec![
0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20, b'r', 0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20, b'r', b'a',
b'a', b't', b'e', b's', b't', b'e', b's',
], ],
})], })],
}; };
@ -742,8 +741,8 @@ mod tests {
}; };
assert_eq!(a, b); assert_eq!(a, b);
let a = ContentDisposition::from_raw(&HeaderValue::from_static("unknown-disp-param")) let a =
.unwrap(); ContentDisposition::from_raw(&HeaderValue::from_static("unknown-disp-param")).unwrap();
let b = ContentDisposition { let b = ContentDisposition {
disposition: DispositionType::Ext(String::from("unknown-disp-param")), disposition: DispositionType::Ext(String::from("unknown-disp-param")),
parameters: vec![], parameters: vec![],
@ -782,8 +781,7 @@ mod tests {
Mainstream browsers like Firefox (gecko) and Chrome use UTF-8 directly as above. Mainstream browsers like Firefox (gecko) and Chrome use UTF-8 directly as above.
(And now, only UTF-8 is handled by this implementation.) (And now, only UTF-8 is handled by this implementation.)
*/ */
let a = let a = HeaderValue::from_str("form-data; name=upload; filename=\"文件.webp\"").unwrap();
HeaderValue::from_str("form-data; name=upload; filename=\"文件.webp\"").unwrap();
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition { let b = ContentDisposition {
disposition: DispositionType::FormData, disposition: DispositionType::FormData,
@ -803,9 +801,7 @@ mod tests {
disposition: DispositionType::FormData, disposition: DispositionType::FormData,
parameters: vec![ parameters: vec![
DispositionParam::Name(String::from("upload")), DispositionParam::Name(String::from("upload")),
DispositionParam::Filename(String::from( DispositionParam::Filename(String::from("余固知謇謇之為患兮,忍而不能舍也.pptx")),
"余固知謇謇之為患兮,忍而不能舍也.pptx",
)),
], ],
}; };
assert_eq!(a, b); assert_eq!(a, b);
@ -870,8 +866,7 @@ mod tests {
}; };
assert_eq!(a, b); assert_eq!(a, b);
let a = let a = HeaderValue::from_static("form-data; name=photo; filename=\"%74%65%73%74.png\"");
HeaderValue::from_static("form-data; name=photo; filename=\"%74%65%73%74.png\"");
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition { let b = ContentDisposition {
disposition: DispositionType::FormData, disposition: DispositionType::FormData,

View File

@ -127,8 +127,7 @@ impl FromStr for ContentRangeSpec {
fn from_str(s: &str) -> Result<Self, ParseError> { fn from_str(s: &str) -> Result<Self, ParseError> {
let res = match split_in_two(s, ' ') { let res = match split_in_two(s, ' ') {
Some(("bytes", resp)) => { Some(("bytes", resp)) => {
let (range, instance_length) = let (range, instance_length) = split_in_two(resp, '/').ok_or(ParseError::Header)?;
split_in_two(resp, '/').ok_or(ParseError::Header)?;
let instance_length = if instance_length == "*" { let instance_length = if instance_length == "*" {
None None

View File

@ -1,110 +1,104 @@
use super::CONTENT_TYPE;
use mime::Mime; use mime::Mime;
use super::CONTENT_TYPE;
crate::http::header::common_header! { crate::http::header::common_header! {
/// `Content-Type` header, defined /// `Content-Type` header, defined in [RFC 9110 §8.3].
/// in [RFC 7231 §3.1.1.5](https://datatracker.ietf.org/doc/html/rfc7231#section-3.1.1.5)
/// ///
/// The `Content-Type` header field indicates the media type of the /// The `Content-Type` header field indicates the media type of the associated representation:
/// associated representation: either the representation enclosed in the /// either the representation enclosed in the message payload or the selected representation,
/// message payload or the selected representation, as determined by the /// as determined by the message semantics. The indicated media type defines both the data
/// message semantics. The indicated media type defines both the data /// format and how that data is intended to be processed by a recipient, within the scope of the
/// format and how that data is intended to be processed by a recipient, /// received message semantics, after any content codings indicated by Content-Encoding are
/// within the scope of the received message semantics, after any content /// decoded.
/// codings indicated by Content-Encoding are decoded.
/// ///
/// Although the `mime` crate allows the mime options to be any slice, this crate /// Although the `mime` crate allows the mime options to be any slice, this crate forces the use
/// forces the use of Vec. This is to make sure the same header can't have more than 1 type. If /// of Vec. This is to make sure the same header can't have more than 1 type. If this is an
/// this is an issue, it's possible to implement `Header` on a custom struct. /// issue, it's possible to implement `Header` on a custom struct.
/// ///
/// # ABNF /// # ABNF
///
/// ```plain /// ```plain
/// Content-Type = media-type /// Content-Type = media-type
/// ``` /// ```
/// ///
/// # Example Values /// # Example Values
/// * `text/html; charset=utf-8` ///
/// * `application/json` /// - `text/html; charset=utf-8`
/// - `application/json`
/// ///
/// # Examples /// # Examples
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::ContentType;
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// ContentType::json()
/// );
/// ```
/// ///
/// ``` /// ```
/// use actix_web::HttpResponse; /// use actix_web::{http::header::ContentType, HttpResponse};
/// use actix_web::http::header::ContentType;
/// ///
/// let mut builder = HttpResponse::Ok(); /// let res_json = HttpResponse::Ok()
/// builder.insert_header( /// .insert_header(ContentType::json());
/// ContentType(mime::TEXT_HTML) ///
/// ); /// let res_html = HttpResponse::Ok()
/// .insert_header(ContentType(mime::TEXT_HTML));
/// ``` /// ```
///
/// [RFC 9110 §8.3]: https://datatracker.ietf.org/doc/html/rfc9110#section-8.3
(ContentType, CONTENT_TYPE) => [Mime] (ContentType, CONTENT_TYPE) => [Mime]
test_parse_and_format { test_parse_and_format {
crate::http::header::common_header_test!( crate::http::header::common_header_test!(
test1, test_text_html,
vec![b"text/html"], vec![b"text/html"],
Some(HeaderField(mime::TEXT_HTML))); Some(HeaderField(mime::TEXT_HTML)));
crate::http::header::common_header_test!(
test_image_star,
vec![b"image/*"],
Some(HeaderField(mime::IMAGE_STAR)));
} }
} }
impl ContentType { impl ContentType {
/// A constructor to easily create a `Content-Type: application/json` /// Constructs a `Content-Type: application/json` header.
/// header.
#[inline] #[inline]
pub fn json() -> ContentType { pub fn json() -> ContentType {
ContentType(mime::APPLICATION_JSON) ContentType(mime::APPLICATION_JSON)
} }
/// A constructor to easily create a `Content-Type: text/plain; /// Constructs a `Content-Type: text/plain; charset=utf-8` header.
/// charset=utf-8` header.
#[inline] #[inline]
pub fn plaintext() -> ContentType { pub fn plaintext() -> ContentType {
ContentType(mime::TEXT_PLAIN_UTF_8) ContentType(mime::TEXT_PLAIN_UTF_8)
} }
/// A constructor to easily create a `Content-Type: text/html; charset=utf-8` /// Constructs a `Content-Type: text/html; charset=utf-8` header.
/// header.
#[inline] #[inline]
pub fn html() -> ContentType { pub fn html() -> ContentType {
ContentType(mime::TEXT_HTML_UTF_8) ContentType(mime::TEXT_HTML_UTF_8)
} }
/// A constructor to easily create a `Content-Type: text/xml` header. /// Constructs a `Content-Type: text/xml` header.
#[inline] #[inline]
pub fn xml() -> ContentType { pub fn xml() -> ContentType {
ContentType(mime::TEXT_XML) ContentType(mime::TEXT_XML)
} }
/// A constructor to easily create a `Content-Type: /// Constructs a `Content-Type: application/www-form-url-encoded` header.
/// application/www-form-url-encoded` header.
#[inline] #[inline]
pub fn form_url_encoded() -> ContentType { pub fn form_url_encoded() -> ContentType {
ContentType(mime::APPLICATION_WWW_FORM_URLENCODED) ContentType(mime::APPLICATION_WWW_FORM_URLENCODED)
} }
/// A constructor to easily create a `Content-Type: image/jpeg` header. /// Constructs a `Content-Type: image/jpeg` header.
#[inline] #[inline]
pub fn jpeg() -> ContentType { pub fn jpeg() -> ContentType {
ContentType(mime::IMAGE_JPEG) ContentType(mime::IMAGE_JPEG)
} }
/// A constructor to easily create a `Content-Type: image/png` header. /// Constructs a `Content-Type: image/png` header.
#[inline] #[inline]
pub fn png() -> ContentType { pub fn png() -> ContentType {
ContentType(mime::IMAGE_PNG) ContentType(mime::IMAGE_PNG)
} }
/// A constructor to easily create a `Content-Type: /// Constructs a `Content-Type: application/octet-stream` header.
/// application/octet-stream` header.
#[inline] #[inline]
pub fn octet_stream() -> ContentType { pub fn octet_stream() -> ContentType {
ContentType(mime::APPLICATION_OCTET_STREAM) ContentType(mime::APPLICATION_OCTET_STREAM)

View File

@ -1,6 +1,7 @@
use super::{HttpDate, DATE};
use std::time::SystemTime; use std::time::SystemTime;
use super::{HttpDate, DATE};
crate::http::header::common_header! { crate::http::header::common_header! {
/// `Date` header, defined /// `Date` header, defined
/// in [RFC 7231 §7.1.1.2](https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.1.2) /// in [RFC 7231 §7.1.1.2](https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.1.2)

View File

@ -152,9 +152,7 @@ impl FromStr for EntityTag {
return Err(crate::error::ParseError::Header); return Err(crate::error::ParseError::Header);
} }
// The etag is weak if its first char is not a DQUOTE. // The etag is weak if its first char is not a DQUOTE.
if slice.len() >= 2 if slice.len() >= 2 && slice.starts_with('"') && check_slice_validity(&slice[1..length - 1])
&& slice.starts_with('"')
&& check_slice_validity(&slice[1..length - 1])
{ {
// No need to check if the last char is a DQUOTE, // No need to check if the last char is a DQUOTE,
// we already did that above. // we already did that above.

View File

@ -4,9 +4,7 @@ use super::{
from_one_raw_str, EntityTag, Header, HeaderName, HeaderValue, HttpDate, InvalidHeaderValue, from_one_raw_str, EntityTag, Header, HeaderName, HeaderValue, HttpDate, InvalidHeaderValue,
TryIntoHeaderValue, Writer, TryIntoHeaderValue, Writer,
}; };
use crate::error::ParseError; use crate::{error::ParseError, http::header, HttpMessage};
use crate::http::header;
use crate::HttpMessage;
/// `If-Range` header, defined /// `If-Range` header, defined
/// in [RFC 7233 §3.2](https://datatracker.ietf.org/doc/html/rfc7233#section-3.2) /// in [RFC 7233 §3.2](https://datatracker.ietf.org/doc/html/rfc7233#section-3.2)

View File

@ -314,7 +314,7 @@ macro_rules! common_header {
}; };
} }
pub(crate) use {common_header, common_header_test_module}; pub(crate) use common_header;
#[cfg(test)] #[cfg(test)]
pub(crate) use common_header_test; pub(crate) use common_header_test;
pub(crate) use common_header_test_module;

View File

@ -6,8 +6,6 @@
use std::fmt; use std::fmt;
use bytes::{Bytes, BytesMut};
// re-export from actix-http // re-export from actix-http
// - header name / value types // - header name / value types
// - relevant traits for converting to header name / value // - relevant traits for converting to header name / value
@ -16,6 +14,7 @@ use bytes::{Bytes, BytesMut};
// - the few typed headers from actix-http // - the few typed headers from actix-http
// - header parsing utils // - header parsing utils
pub use actix_http::header::*; pub use actix_http::header::*;
use bytes::{Bytes, BytesMut};
mod accept; mod accept;
mod accept_charset; mod accept_charset;
@ -43,32 +42,33 @@ mod preference;
mod range; mod range;
#[cfg(test)] #[cfg(test)]
pub(crate) use macros::common_header_test; pub(crate) use self::macros::common_header_test;
pub(crate) use macros::{common_header, common_header_test_module}; pub(crate) use self::macros::{common_header, common_header_test_module};
pub use self::{
pub use self::accept::Accept; accept::Accept,
pub use self::accept_charset::AcceptCharset; accept_charset::AcceptCharset,
pub use self::accept_encoding::AcceptEncoding; accept_encoding::AcceptEncoding,
pub use self::accept_language::AcceptLanguage; accept_language::AcceptLanguage,
pub use self::allow::Allow; allow::Allow,
pub use self::cache_control::{CacheControl, CacheDirective}; cache_control::{CacheControl, CacheDirective},
pub use self::content_disposition::{ContentDisposition, DispositionParam, DispositionType}; content_disposition::{ContentDisposition, DispositionParam, DispositionType},
pub use self::content_language::ContentLanguage; content_language::ContentLanguage,
pub use self::content_range::{ContentRange, ContentRangeSpec}; content_range::{ContentRange, ContentRangeSpec},
pub use self::content_type::ContentType; content_type::ContentType,
pub use self::date::Date; date::Date,
pub use self::encoding::Encoding; encoding::Encoding,
pub use self::entity::EntityTag; entity::EntityTag,
pub use self::etag::ETag; etag::ETag,
pub use self::expires::Expires; expires::Expires,
pub use self::if_match::IfMatch; if_match::IfMatch,
pub use self::if_modified_since::IfModifiedSince; if_modified_since::IfModifiedSince,
pub use self::if_none_match::IfNoneMatch; if_none_match::IfNoneMatch,
pub use self::if_range::IfRange; if_range::IfRange,
pub use self::if_unmodified_since::IfUnmodifiedSince; if_unmodified_since::IfUnmodifiedSince,
pub use self::last_modified::LastModified; last_modified::LastModified,
pub use self::preference::Preference; preference::Preference,
pub use self::range::{ByteRangeSpec, Range}; range::{ByteRangeSpec, Range},
};
/// Format writer ([`fmt::Write`]) for a [`BytesMut`]. /// Format writer ([`fmt::Write`]) for a [`BytesMut`].
#[derive(Debug, Default)] #[derive(Debug, Default)]

View File

@ -74,6 +74,11 @@
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]
pub use actix_http::{body, HttpMessage};
#[cfg(feature = "cookies")]
#[doc(inline)]
pub use cookie;
mod app; mod app;
mod app_service; mod app_service;
mod config; mod config;
@ -102,25 +107,21 @@ pub mod test;
pub(crate) mod types; pub(crate) mod types;
pub mod web; pub mod web;
pub use crate::app::App;
#[doc(inline)] #[doc(inline)]
pub use crate::error::Result; pub use crate::error::Result;
pub use crate::error::{Error, ResponseError}; pub use crate::{
pub use crate::extract::FromRequest; app::App,
pub use crate::handler::Handler; error::{Error, ResponseError},
pub use crate::request::HttpRequest; extract::FromRequest,
pub use crate::resource::Resource; handler::Handler,
pub use crate::response::{CustomizeResponder, HttpResponse, HttpResponseBuilder, Responder}; request::HttpRequest,
pub use crate::route::Route; resource::Resource,
pub use crate::scope::Scope; response::{CustomizeResponder, HttpResponse, HttpResponseBuilder, Responder},
pub use crate::server::HttpServer; route::Route,
pub use crate::types::Either; scope::Scope,
server::HttpServer,
pub use actix_http::{body, HttpMessage}; types::Either,
};
#[cfg(feature = "cookies")]
#[doc(inline)]
pub use cookie;
macro_rules! codegen_reexport { macro_rules! codegen_reexport {
($name:ident) => { ($name:ident) => {

View File

@ -146,10 +146,9 @@ mod tests {
// easier to code when cookies feature is disabled // easier to code when cookies feature is disabled
#![allow(unused_imports)] #![allow(unused_imports)]
use super::*;
use actix_service::IntoService; use actix_service::IntoService;
use super::*;
use crate::{ use crate::{
dev::ServiceRequest, dev::ServiceRequest,
http::StatusCode, http::StatusCode,
@ -207,9 +206,9 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_condition_scope_middleware() { async fn test_condition_scope_middleware() {
let srv = |req: ServiceRequest| { let srv = |req: ServiceRequest| {
Box::pin(async move { Box::pin(
Ok(req.into_response(HttpResponse::InternalServerError().finish())) async move { Ok(req.into_response(HttpResponse::InternalServerError().finish())) },
}) )
}; };
let logger = Logger::default(); let logger = Logger::default();

View File

@ -11,13 +11,14 @@ use actix_http::encoding::Encoder;
use actix_service::{Service, Transform}; use actix_service::{Service, Transform};
use actix_utils::future::{ok, Either, Ready}; use actix_utils::future::{ok, Either, Ready};
use futures_core::ready; use futures_core::ready;
use mime::Mime;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
use crate::{ use crate::{
body::{EitherBody, MessageBody}, body::{EitherBody, MessageBody},
http::{ http::{
header::{self, AcceptEncoding, Encoding, HeaderValue}, header::{self, AcceptEncoding, ContentEncoding, Encoding, HeaderValue},
StatusCode, StatusCode,
}, },
service::{ServiceRequest, ServiceResponse}, service::{ServiceRequest, ServiceResponse},
@ -170,19 +171,40 @@ where
{ {
type Output = Result<ServiceResponse<EitherBody<Encoder<B>>>, Error>; type Output = Result<ServiceResponse<EitherBody<Encoder<B>>>, Error>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project(); let this = self.as_mut().project();
match ready!(this.fut.poll(cx)) { match ready!(this.fut.poll(cx)) {
Ok(resp) => { Ok(resp) => {
let enc = match this.encoding { let enc = match this.encoding {
Encoding::Known(enc) => *enc, Encoding::Known(enc) => *enc,
Encoding::Unknown(enc) => { Encoding::Unknown(enc) => {
unimplemented!("encoding {} should not be here", enc); unimplemented!("encoding '{enc}' should not be here");
} }
}; };
Poll::Ready(Ok(resp.map_body(move |head, body| { Poll::Ready(Ok(resp.map_body(move |head, body| {
let content_type = head.headers.get(header::CONTENT_TYPE);
fn default_compress_predicate(content_type: Option<&HeaderValue>) -> bool {
match content_type {
None => true,
Some(hdr) => {
match hdr.to_str().ok().and_then(|hdr| hdr.parse::<Mime>().ok()) {
Some(mime) if mime.type_().as_str() == "image" => false,
Some(mime) if mime.type_().as_str() == "video" => false,
_ => true,
}
}
}
}
let enc = if default_compress_predicate(content_type) {
enc
} else {
ContentEncoding::Identity
};
EitherBody::left(Encoder::response(enc, head, body)) EitherBody::left(Encoder::response(enc, head, body))
}))) })))
} }
@ -246,8 +268,18 @@ static SUPPORTED_ENCODINGS: &[Encoding] = &[
mod tests { mod tests {
use std::collections::HashSet; use std::collections::HashSet;
use static_assertions::assert_impl_all;
use super::*; use super::*;
use crate::{middleware::DefaultHeaders, test, web, App}; use crate::{http::header::ContentType, middleware::DefaultHeaders, test, web, App};
const HTML_DATA_PART: &str = "<html><h1>hello world</h1></html";
const HTML_DATA: &str = const_str::repeat!(HTML_DATA_PART, 100);
const TEXT_DATA_PART: &str = "hello world ";
const TEXT_DATA: &str = const_str::repeat!(TEXT_DATA_PART, 100);
assert_impl_all!(Compress: Send, Sync);
pub fn gzip_decode(bytes: impl AsRef<[u8]>) -> Vec<u8> { pub fn gzip_decode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
use std::io::Read as _; use std::io::Read as _;
@ -257,23 +289,55 @@ mod tests {
buf buf
} }
#[track_caller]
fn assert_successful_res_with_content_type<B>(res: &ServiceResponse<B>, ct: &str) {
assert!(res.status().is_success());
assert!(
res.headers()
.get(header::CONTENT_TYPE)
.expect("content-type header should be present")
.to_str()
.expect("content-type header should be utf-8")
.contains(ct),
"response's content-type did not match {}",
ct
);
}
#[track_caller]
fn assert_successful_gzip_res_with_content_type<B>(res: &ServiceResponse<B>, ct: &str) {
assert_successful_res_with_content_type(res, ct);
assert_eq!(
res.headers()
.get(header::CONTENT_ENCODING)
.expect("response should be gzip compressed"),
"gzip",
);
}
#[track_caller]
fn assert_successful_identity_res_with_content_type<B>(res: &ServiceResponse<B>, ct: &str) {
assert_successful_res_with_content_type(res, ct);
assert!(
res.headers().get(header::CONTENT_ENCODING).is_none(),
"response should not be compressed",
);
}
#[actix_rt::test] #[actix_rt::test]
async fn prevents_double_compressing() { async fn prevents_double_compressing() {
const D: &str = "hello world ";
const DATA: &str = const_str::repeat!(D, 100);
let app = test::init_service({ let app = test::init_service({
App::new() App::new()
.wrap(Compress::default()) .wrap(Compress::default())
.route( .route(
"/single", "/single",
web::get().to(move || HttpResponse::Ok().body(DATA)), web::get().to(move || HttpResponse::Ok().body(TEXT_DATA)),
) )
.service( .service(
web::resource("/double") web::resource("/double")
.wrap(Compress::default()) .wrap(Compress::default())
.wrap(DefaultHeaders::new().add(("x-double", "true"))) .wrap(DefaultHeaders::new().add(("x-double", "true")))
.route(web::get().to(move || HttpResponse::Ok().body(DATA))), .route(web::get().to(move || HttpResponse::Ok().body(TEXT_DATA))),
) )
}) })
.await; .await;
@ -287,7 +351,7 @@ mod tests {
assert_eq!(res.headers().get("x-double"), None); assert_eq!(res.headers().get("x-double"), None);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "gzip"); assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "gzip");
let bytes = test::read_body(res).await; let bytes = test::read_body(res).await;
assert_eq!(gzip_decode(bytes), DATA.as_bytes()); assert_eq!(gzip_decode(bytes), TEXT_DATA.as_bytes());
let req = test::TestRequest::default() let req = test::TestRequest::default()
.uri("/double") .uri("/double")
@ -298,7 +362,7 @@ mod tests {
assert_eq!(res.headers().get("x-double").unwrap(), "true"); assert_eq!(res.headers().get("x-double").unwrap(), "true");
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "gzip"); assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "gzip");
let bytes = test::read_body(res).await; let bytes = test::read_body(res).await;
assert_eq!(gzip_decode(bytes), DATA.as_bytes()); assert_eq!(gzip_decode(bytes), TEXT_DATA.as_bytes());
} }
#[actix_rt::test] #[actix_rt::test]
@ -324,4 +388,45 @@ mod tests {
assert!(vary_headers.contains(&HeaderValue::from_static("x-test"))); assert!(vary_headers.contains(&HeaderValue::from_static("x-test")));
assert!(vary_headers.contains(&HeaderValue::from_static("accept-encoding"))); assert!(vary_headers.contains(&HeaderValue::from_static("accept-encoding")));
} }
fn configure_predicate_test(cfg: &mut web::ServiceConfig) {
cfg.route(
"/html",
web::get().to(|| {
HttpResponse::Ok()
.content_type(ContentType::html())
.body(HTML_DATA)
}),
)
.route(
"/image",
web::get().to(|| {
HttpResponse::Ok()
.content_type(ContentType::jpeg())
.body(TEXT_DATA)
}),
);
}
#[actix_rt::test]
async fn prevents_compression_jpeg() {
let app = test::init_service(
App::new()
.wrap(Compress::default())
.configure(configure_predicate_test),
)
.await;
let req =
test::TestRequest::with_uri("/html").insert_header((header::ACCEPT_ENCODING, "gzip"));
let res = test::call_service(&app, req.to_request()).await;
assert_successful_gzip_res_with_content_type(&res, "text/html");
assert_ne!(test::read_body(res).await, HTML_DATA.as_bytes());
let req =
test::TestRequest::with_uri("/image").insert_header((header::ACCEPT_ENCODING, "gzip"));
let res = test::call_service(&app, req.to_request()).await;
assert_successful_identity_res_with_content_type(&res, "image/jpeg");
assert_eq!(test::read_body(res).await, TEXT_DATA.as_bytes());
}
} }

Some files were not shown because too many files have changed in this diff Show More