mirror of
https://github.com/fafhrd91/actix-web
synced 2025-01-31 19:10:07 +01:00
Merge branch 'actix:master' into fix-compression-middleware-images
This commit is contained in:
commit
42fd6290d6
2
.github/workflows/bench.yml
vendored
2
.github/workflows/bench.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
|
6
.github/workflows/ci-post-merge.yml
vendored
6
.github/workflows/ci-post-merge.yml
vendored
@ -29,7 +29,7 @@ jobs:
|
|||||||
CARGO_UNSTABLE_SPARSE_REGISTRY: true
|
CARGO_UNSTABLE_SPARSE_REGISTRY: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
# install OpenSSL on Windows
|
# install OpenSSL on Windows
|
||||||
# TODO: GitHub actions docs state that OpenSSL is
|
# TODO: GitHub actions docs state that OpenSSL is
|
||||||
@ -93,7 +93,7 @@ jobs:
|
|||||||
CARGO_INCREMENTAL: 0
|
CARGO_INCREMENTAL: 0
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
@ -120,7 +120,7 @@ jobs:
|
|||||||
CARGO_INCREMENTAL: 0
|
CARGO_INCREMENTAL: 0
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@ -31,7 +31,7 @@ jobs:
|
|||||||
VCPKGRS_DYNAMIC: 1
|
VCPKGRS_DYNAMIC: 1
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
# install OpenSSL on Windows
|
# install OpenSSL on Windows
|
||||||
# TODO: GitHub actions docs state that OpenSSL is
|
# TODO: GitHub actions docs state that OpenSSL is
|
||||||
@ -102,7 +102,7 @@ jobs:
|
|||||||
name: io-uring tests
|
name: io-uring tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
@ -123,7 +123,7 @@ jobs:
|
|||||||
name: doc tests
|
name: doc tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@nightly
|
- uses: dtolnay/rust-toolchain@nightly
|
||||||
|
|
||||||
|
6
.github/workflows/clippy-fmt.yml
vendored
6
.github/workflows/clippy-fmt.yml
vendored
@ -8,7 +8,7 @@ jobs:
|
|||||||
fmt:
|
fmt:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- uses: dtolnay/rust-toolchain@nightly
|
- uses: dtolnay/rust-toolchain@nightly
|
||||||
with: { components: rustfmt }
|
with: { components: rustfmt }
|
||||||
- run: cargo fmt --all -- --check
|
- run: cargo fmt --all -- --check
|
||||||
@ -16,7 +16,7 @@ jobs:
|
|||||||
clippy:
|
clippy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with: { components: clippy }
|
with: { components: clippy }
|
||||||
@ -35,7 +35,7 @@ jobs:
|
|||||||
lint-docs:
|
lint-docs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with: { components: rust-docs }
|
with: { components: rust-docs }
|
||||||
|
2
.github/workflows/coverage.yml
vendored
2
.github/workflows/coverage.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
|||||||
name: coverage
|
name: coverage
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Install stable
|
- name: Install stable
|
||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
|
2
.github/workflows/upload-doc.yml
vendored
2
.github/workflows/upload-doc.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@nightly
|
- uses: dtolnay/rust-toolchain@nightly
|
||||||
|
|
||||||
|
@ -32,11 +32,11 @@ derive_more = "0.99.5"
|
|||||||
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
||||||
http-range = "0.1.4"
|
http-range = "0.1.4"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
mime = "0.3"
|
mime = "0.3.9"
|
||||||
mime_guess = "2.0.1"
|
mime_guess = "2.0.1"
|
||||||
percent-encoding = "2.1"
|
percent-encoding = "2.1"
|
||||||
pin-project-lite = "0.2.7"
|
pin-project-lite = "0.2.7"
|
||||||
v_htmlescape= "0.15"
|
v_htmlescape = "0.15.5"
|
||||||
|
|
||||||
# experimental-io-uring
|
# experimental-io-uring
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
@ -4,46 +4,45 @@ use derive_more::Display;
|
|||||||
/// Errors which can occur when serving static files.
|
/// Errors which can occur when serving static files.
|
||||||
#[derive(Debug, PartialEq, Eq, Display)]
|
#[derive(Debug, PartialEq, Eq, Display)]
|
||||||
pub enum FilesError {
|
pub enum FilesError {
|
||||||
/// Path is not a directory
|
/// Path is not a directory.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[display(fmt = "Path is not a directory. Unable to serve static files")]
|
#[display(fmt = "path is not a directory. Unable to serve static files")]
|
||||||
IsNotDirectory,
|
IsNotDirectory,
|
||||||
|
|
||||||
/// Cannot render directory
|
/// Cannot render directory.
|
||||||
#[display(fmt = "Unable to render directory without index file")]
|
#[display(fmt = "unable to render directory without index file")]
|
||||||
IsDirectory,
|
IsDirectory,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return `NotFound` for `FilesError`
|
|
||||||
impl ResponseError for FilesError {
|
impl ResponseError for FilesError {
|
||||||
|
/// Returns `404 Not Found`.
|
||||||
fn status_code(&self) -> StatusCode {
|
fn status_code(&self) -> StatusCode {
|
||||||
StatusCode::NOT_FOUND
|
StatusCode::NOT_FOUND
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::enum_variant_names)]
|
|
||||||
#[derive(Debug, PartialEq, Eq, Display)]
|
#[derive(Debug, PartialEq, Eq, Display)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum UriSegmentError {
|
pub enum UriSegmentError {
|
||||||
/// The segment started with the wrapped invalid character.
|
/// Segment started with the wrapped invalid character.
|
||||||
#[display(fmt = "The segment started with the wrapped invalid character")]
|
#[display(fmt = "segment started with invalid character: ('{_0}')")]
|
||||||
BadStart(char),
|
BadStart(char),
|
||||||
|
|
||||||
/// The segment contained the wrapped invalid character.
|
/// Segment contained the wrapped invalid character.
|
||||||
#[display(fmt = "The segment contained the wrapped invalid character")]
|
#[display(fmt = "segment contained invalid character ('{_0}')")]
|
||||||
BadChar(char),
|
BadChar(char),
|
||||||
|
|
||||||
/// The segment ended with the wrapped invalid character.
|
/// Segment ended with the wrapped invalid character.
|
||||||
#[display(fmt = "The segment ended with the wrapped invalid character")]
|
#[display(fmt = "segment ended with invalid character: ('{_0}')")]
|
||||||
BadEnd(char),
|
BadEnd(char),
|
||||||
|
|
||||||
/// The path is not a valid UTF-8 string after doing percent decoding.
|
/// Path is not a valid UTF-8 string after percent-decoding.
|
||||||
#[display(fmt = "The path is not a valid UTF-8 string after percent-decoding")]
|
#[display(fmt = "path is not a valid UTF-8 string after percent-decoding")]
|
||||||
NotValidUtf8,
|
NotValidUtf8,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return `BadRequest` for `UriSegmentError`
|
|
||||||
impl ResponseError for UriSegmentError {
|
impl ResponseError for UriSegmentError {
|
||||||
|
/// Returns `400 Bad Request`.
|
||||||
fn status_code(&self) -> StatusCode {
|
fn status_code(&self) -> StatusCode {
|
||||||
StatusCode::BAD_REQUEST
|
StatusCode::BAD_REQUEST
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,36 @@
|
|||||||
use derive_more::{Display, Error};
|
use std::fmt;
|
||||||
|
|
||||||
|
use derive_more::Error;
|
||||||
|
|
||||||
|
/// Copy of `http_range::HttpRangeParseError`.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum HttpRangeParseError {
|
||||||
|
InvalidRange,
|
||||||
|
NoOverlap,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<http_range::HttpRangeParseError> for HttpRangeParseError {
|
||||||
|
fn from(err: http_range::HttpRangeParseError) -> Self {
|
||||||
|
match err {
|
||||||
|
http_range::HttpRangeParseError::InvalidRange => Self::InvalidRange,
|
||||||
|
http_range::HttpRangeParseError::NoOverlap => Self::NoOverlap,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Error)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub struct ParseRangeErr(#[error(not(source))] HttpRangeParseError);
|
||||||
|
|
||||||
|
impl fmt::Display for ParseRangeErr {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str("invalid Range header: ")?;
|
||||||
|
f.write_str(match self.0 {
|
||||||
|
HttpRangeParseError::InvalidRange => "invalid syntax",
|
||||||
|
HttpRangeParseError::NoOverlap => "range starts after end of content",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// HTTP Range header representation.
|
/// HTTP Range header representation.
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
@ -10,26 +42,22 @@ pub struct HttpRange {
|
|||||||
pub length: u64,
|
pub length: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Display, Error)]
|
|
||||||
#[display(fmt = "Parse HTTP Range failed")]
|
|
||||||
pub struct ParseRangeErr(#[error(not(source))] ());
|
|
||||||
|
|
||||||
impl HttpRange {
|
impl HttpRange {
|
||||||
/// Parses Range HTTP header string as per RFC 2616.
|
/// Parses Range HTTP header string as per RFC 2616.
|
||||||
///
|
///
|
||||||
/// `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> {
|
||||||
match http_range::HttpRange::parse(header, size) {
|
let ranges = http_range::HttpRange::parse(header, size)
|
||||||
Ok(ranges) => Ok(ranges
|
.map_err(|err| ParseRangeErr(err.into()))?;
|
||||||
|
|
||||||
|
Ok(ranges
|
||||||
.iter()
|
.iter()
|
||||||
.map(|range| HttpRange {
|
.map(|range| HttpRange {
|
||||||
start: range.start,
|
start: range.start,
|
||||||
length: range.length,
|
length: range.length,
|
||||||
})
|
})
|
||||||
.collect()),
|
.collect())
|
||||||
Err(_) => Err(ParseRangeErr(())),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,8 +42,8 @@ futures-core = { version = "0.3.17", default-features = false }
|
|||||||
http = "0.2.7"
|
http = "0.2.7"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
socket2 = "0.4"
|
socket2 = "0.4"
|
||||||
serde = "1.0"
|
serde = "1"
|
||||||
serde_json = "1.0"
|
serde_json = "1"
|
||||||
slab = "0.4"
|
slab = "0.4"
|
||||||
serde_urlencoded = "0.7"
|
serde_urlencoded = "0.7"
|
||||||
tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
|
tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
|
||||||
|
@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
## Unreleased - 2023-xx-xx
|
## Unreleased - 2023-xx-xx
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add `body::to_body_limit()` function.
|
||||||
|
- Add `body::BodyLimitExceeded` error type.
|
||||||
|
|
||||||
## 3.3.1 - 2023-03-02
|
## 3.3.1 - 2023-03-02
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
@ -73,7 +73,7 @@ httparse = "1.5.1"
|
|||||||
httpdate = "1.0.1"
|
httpdate = "1.0.1"
|
||||||
itoa = "1"
|
itoa = "1"
|
||||||
language-tags = "0.3"
|
language-tags = "0.3"
|
||||||
mime = "0.3"
|
mime = "0.3.4"
|
||||||
percent-encoding = "2.1"
|
percent-encoding = "2.1"
|
||||||
pin-project-lite = "0.2"
|
pin-project-lite = "0.2"
|
||||||
smallvec = "1.6.1"
|
smallvec = "1.6.1"
|
||||||
@ -126,17 +126,6 @@ name = "ws"
|
|||||||
required-features = ["ws", "rustls"]
|
required-features = ["ws", "rustls"]
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "write-camel-case"
|
name = "response-body-compression"
|
||||||
harness = false
|
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
name = "status-line"
|
|
||||||
harness = false
|
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
name = "uninit-headers"
|
|
||||||
harness = false
|
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
name = "quality-value"
|
|
||||||
harness = false
|
harness = false
|
||||||
|
required-features = ["compress-brotli", "compress-gzip", "compress-zstd"]
|
||||||
|
@ -1,97 +0,0 @@
|
|||||||
#![allow(clippy::uninlined_format_args)]
|
|
||||||
|
|
||||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
|
||||||
|
|
||||||
const CODES: &[u16] = &[0, 1000, 201, 800, 550];
|
|
||||||
|
|
||||||
fn bench_quality_display_impls(c: &mut Criterion) {
|
|
||||||
let mut group = c.benchmark_group("quality value display impls");
|
|
||||||
|
|
||||||
for i in CODES.iter() {
|
|
||||||
group.bench_with_input(BenchmarkId::new("New (fast?)", i), i, |b, &i| {
|
|
||||||
b.iter(|| _new::Quality(i).to_string())
|
|
||||||
});
|
|
||||||
|
|
||||||
group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| {
|
|
||||||
b.iter(|| _naive::Quality(i).to_string())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
group.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
criterion_group!(benches, bench_quality_display_impls);
|
|
||||||
criterion_main!(benches);
|
|
||||||
|
|
||||||
mod _new {
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
pub struct Quality(pub(crate) u16);
|
|
||||||
|
|
||||||
impl fmt::Display for Quality {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self.0 {
|
|
||||||
0 => f.write_str("0"),
|
|
||||||
1000 => f.write_str("1"),
|
|
||||||
|
|
||||||
// some number in the range 1–999
|
|
||||||
x => {
|
|
||||||
f.write_str("0.")?;
|
|
||||||
|
|
||||||
// this implementation avoids string allocation otherwise required
|
|
||||||
// for `.trim_end_matches('0')`
|
|
||||||
|
|
||||||
if x < 10 {
|
|
||||||
f.write_str("00")?;
|
|
||||||
// 0 is handled so it's not possible to have a trailing 0, we can just return
|
|
||||||
itoa_fmt(f, x)
|
|
||||||
} else if x < 100 {
|
|
||||||
f.write_str("0")?;
|
|
||||||
if x % 10 == 0 {
|
|
||||||
// trailing 0, divide by 10 and write
|
|
||||||
itoa_fmt(f, x / 10)
|
|
||||||
} else {
|
|
||||||
itoa_fmt(f, x)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// x is in range 101–999
|
|
||||||
|
|
||||||
if x % 100 == 0 {
|
|
||||||
// two trailing 0s, divide by 100 and write
|
|
||||||
itoa_fmt(f, x / 100)
|
|
||||||
} else if x % 10 == 0 {
|
|
||||||
// one trailing 0, divide by 10 and write
|
|
||||||
itoa_fmt(f, x / 10)
|
|
||||||
} else {
|
|
||||||
itoa_fmt(f, x)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn itoa_fmt<W: fmt::Write, V: itoa::Integer>(mut wr: W, value: V) -> fmt::Result {
|
|
||||||
let mut buf = itoa::Buffer::new();
|
|
||||||
wr.write_str(buf.format(value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod _naive {
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
pub struct Quality(pub(crate) u16);
|
|
||||||
|
|
||||||
impl fmt::Display for Quality {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self.0 {
|
|
||||||
0 => f.write_str("0"),
|
|
||||||
1000 => f.write_str("1"),
|
|
||||||
|
|
||||||
x => {
|
|
||||||
write!(f, "{}", format!("{:03}", x).trim_end_matches('0'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
90
actix-http/benches/response-body-compression.rs
Normal file
90
actix-http/benches/response-body-compression.rs
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
#![allow(clippy::uninlined_format_args)]
|
||||||
|
|
||||||
|
use std::convert::Infallible;
|
||||||
|
|
||||||
|
use actix_http::{encoding::Encoder, ContentEncoding, Request, Response, StatusCode};
|
||||||
|
use actix_service::{fn_service, Service as _};
|
||||||
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
|
|
||||||
|
static BODY: &[u8] = include_bytes!("../Cargo.toml");
|
||||||
|
|
||||||
|
fn compression_responses(c: &mut Criterion) {
|
||||||
|
let mut group = c.benchmark_group("compression responses");
|
||||||
|
|
||||||
|
group.bench_function("identity", |b| {
|
||||||
|
let rt = actix_rt::Runtime::new().unwrap();
|
||||||
|
|
||||||
|
let identity_svc = fn_service(|_: Request| async move {
|
||||||
|
let mut res = Response::with_body(StatusCode::OK, ());
|
||||||
|
let body = black_box(Encoder::response(
|
||||||
|
ContentEncoding::Identity,
|
||||||
|
res.head_mut(),
|
||||||
|
BODY,
|
||||||
|
));
|
||||||
|
Ok::<_, Infallible>(black_box(res.set_body(black_box(body))))
|
||||||
|
});
|
||||||
|
|
||||||
|
b.iter(|| {
|
||||||
|
rt.block_on(identity_svc.call(Request::new())).unwrap();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group.bench_function("gzip", |b| {
|
||||||
|
let rt = actix_rt::Runtime::new().unwrap();
|
||||||
|
|
||||||
|
let identity_svc = fn_service(|_: Request| async move {
|
||||||
|
let mut res = Response::with_body(StatusCode::OK, ());
|
||||||
|
let body = black_box(Encoder::response(
|
||||||
|
ContentEncoding::Gzip,
|
||||||
|
res.head_mut(),
|
||||||
|
BODY,
|
||||||
|
));
|
||||||
|
Ok::<_, Infallible>(black_box(res.set_body(black_box(body))))
|
||||||
|
});
|
||||||
|
|
||||||
|
b.iter(|| {
|
||||||
|
rt.block_on(identity_svc.call(Request::new())).unwrap();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group.bench_function("br", |b| {
|
||||||
|
let rt = actix_rt::Runtime::new().unwrap();
|
||||||
|
|
||||||
|
let identity_svc = fn_service(|_: Request| async move {
|
||||||
|
let mut res = Response::with_body(StatusCode::OK, ());
|
||||||
|
let body = black_box(Encoder::response(
|
||||||
|
ContentEncoding::Brotli,
|
||||||
|
res.head_mut(),
|
||||||
|
BODY,
|
||||||
|
));
|
||||||
|
Ok::<_, Infallible>(black_box(res.set_body(black_box(body))))
|
||||||
|
});
|
||||||
|
|
||||||
|
b.iter(|| {
|
||||||
|
rt.block_on(identity_svc.call(Request::new())).unwrap();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group.bench_function("zstd", |b| {
|
||||||
|
let rt = actix_rt::Runtime::new().unwrap();
|
||||||
|
|
||||||
|
let identity_svc = fn_service(|_: Request| async move {
|
||||||
|
let mut res = Response::with_body(StatusCode::OK, ());
|
||||||
|
let body = black_box(Encoder::response(
|
||||||
|
ContentEncoding::Zstd,
|
||||||
|
res.head_mut(),
|
||||||
|
BODY,
|
||||||
|
));
|
||||||
|
Ok::<_, Infallible>(black_box(res.set_body(black_box(body))))
|
||||||
|
});
|
||||||
|
|
||||||
|
b.iter(|| {
|
||||||
|
rt.block_on(identity_svc.call(Request::new())).unwrap();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(benches, compression_responses);
|
||||||
|
criterion_main!(benches);
|
@ -1,214 +0,0 @@
|
|||||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
|
||||||
|
|
||||||
use bytes::BytesMut;
|
|
||||||
use http::Version;
|
|
||||||
|
|
||||||
const CODES: &[u16] = &[201, 303, 404, 515];
|
|
||||||
|
|
||||||
fn bench_write_status_line_11(c: &mut Criterion) {
|
|
||||||
let mut group = c.benchmark_group("write_status_line v1.1");
|
|
||||||
|
|
||||||
let version = Version::HTTP_11;
|
|
||||||
|
|
||||||
for i in CODES.iter() {
|
|
||||||
group.bench_with_input(BenchmarkId::new("Original (unsafe)", i), i, |b, &i| {
|
|
||||||
b.iter(|| {
|
|
||||||
let mut b = BytesMut::with_capacity(35);
|
|
||||||
_original::write_status_line(version, i, &mut b);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
group.bench_with_input(BenchmarkId::new("New (safe)", i), i, |b, &i| {
|
|
||||||
b.iter(|| {
|
|
||||||
let mut b = BytesMut::with_capacity(35);
|
|
||||||
_new::write_status_line(version, i, &mut b);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| {
|
|
||||||
b.iter(|| {
|
|
||||||
let mut b = BytesMut::with_capacity(35);
|
|
||||||
_naive::write_status_line(version, i, &mut b);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
group.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bench_write_status_line_10(c: &mut Criterion) {
|
|
||||||
let mut group = c.benchmark_group("write_status_line v1.0");
|
|
||||||
|
|
||||||
let version = Version::HTTP_10;
|
|
||||||
|
|
||||||
for i in CODES.iter() {
|
|
||||||
group.bench_with_input(BenchmarkId::new("Original (unsafe)", i), i, |b, &i| {
|
|
||||||
b.iter(|| {
|
|
||||||
let mut b = BytesMut::with_capacity(35);
|
|
||||||
_original::write_status_line(version, i, &mut b);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
group.bench_with_input(BenchmarkId::new("New (safe)", i), i, |b, &i| {
|
|
||||||
b.iter(|| {
|
|
||||||
let mut b = BytesMut::with_capacity(35);
|
|
||||||
_new::write_status_line(version, i, &mut b);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| {
|
|
||||||
b.iter(|| {
|
|
||||||
let mut b = BytesMut::with_capacity(35);
|
|
||||||
_naive::write_status_line(version, i, &mut b);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
group.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bench_write_status_line_09(c: &mut Criterion) {
|
|
||||||
let mut group = c.benchmark_group("write_status_line v0.9");
|
|
||||||
|
|
||||||
let version = Version::HTTP_09;
|
|
||||||
|
|
||||||
for i in CODES.iter() {
|
|
||||||
group.bench_with_input(BenchmarkId::new("Original (unsafe)", i), i, |b, &i| {
|
|
||||||
b.iter(|| {
|
|
||||||
let mut b = BytesMut::with_capacity(35);
|
|
||||||
_original::write_status_line(version, i, &mut b);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
group.bench_with_input(BenchmarkId::new("New (safe)", i), i, |b, &i| {
|
|
||||||
b.iter(|| {
|
|
||||||
let mut b = BytesMut::with_capacity(35);
|
|
||||||
_new::write_status_line(version, i, &mut b);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| {
|
|
||||||
b.iter(|| {
|
|
||||||
let mut b = BytesMut::with_capacity(35);
|
|
||||||
_naive::write_status_line(version, i, &mut b);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
group.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
criterion_group!(
|
|
||||||
benches,
|
|
||||||
bench_write_status_line_11,
|
|
||||||
bench_write_status_line_10,
|
|
||||||
bench_write_status_line_09
|
|
||||||
);
|
|
||||||
criterion_main!(benches);
|
|
||||||
|
|
||||||
mod _naive {
|
|
||||||
use bytes::{BufMut, BytesMut};
|
|
||||||
use http::Version;
|
|
||||||
|
|
||||||
pub(crate) fn write_status_line(version: Version, n: u16, bytes: &mut BytesMut) {
|
|
||||||
match version {
|
|
||||||
Version::HTTP_11 => bytes.put_slice(b"HTTP/1.1 "),
|
|
||||||
Version::HTTP_10 => bytes.put_slice(b"HTTP/1.0 "),
|
|
||||||
Version::HTTP_09 => bytes.put_slice(b"HTTP/0.9 "),
|
|
||||||
_ => {
|
|
||||||
// other HTTP version handlers do not use this method
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bytes.put_slice(n.to_string().as_bytes());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod _new {
|
|
||||||
use bytes::{BufMut, BytesMut};
|
|
||||||
use http::Version;
|
|
||||||
|
|
||||||
const DIGITS_START: u8 = b'0';
|
|
||||||
|
|
||||||
pub(crate) fn write_status_line(version: Version, n: u16, bytes: &mut BytesMut) {
|
|
||||||
match version {
|
|
||||||
Version::HTTP_11 => bytes.put_slice(b"HTTP/1.1 "),
|
|
||||||
Version::HTTP_10 => bytes.put_slice(b"HTTP/1.0 "),
|
|
||||||
Version::HTTP_09 => bytes.put_slice(b"HTTP/0.9 "),
|
|
||||||
_ => {
|
|
||||||
// other HTTP version handlers do not use this method
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let d100 = (n / 100) as u8;
|
|
||||||
let d10 = ((n / 10) % 10) as u8;
|
|
||||||
let d1 = (n % 10) as u8;
|
|
||||||
|
|
||||||
bytes.put_u8(DIGITS_START + d100);
|
|
||||||
bytes.put_u8(DIGITS_START + d10);
|
|
||||||
bytes.put_u8(DIGITS_START + d1);
|
|
||||||
|
|
||||||
bytes.put_u8(b' ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod _original {
|
|
||||||
use std::ptr;
|
|
||||||
|
|
||||||
use bytes::{BufMut, BytesMut};
|
|
||||||
use http::Version;
|
|
||||||
|
|
||||||
const DEC_DIGITS_LUT: &[u8] = b"0001020304050607080910111213141516171819\
|
|
||||||
2021222324252627282930313233343536373839\
|
|
||||||
4041424344454647484950515253545556575859\
|
|
||||||
6061626364656667686970717273747576777879\
|
|
||||||
8081828384858687888990919293949596979899";
|
|
||||||
|
|
||||||
pub(crate) const STATUS_LINE_BUF_SIZE: usize = 13;
|
|
||||||
|
|
||||||
pub(crate) fn write_status_line(version: Version, mut n: u16, bytes: &mut BytesMut) {
|
|
||||||
let mut buf: [u8; STATUS_LINE_BUF_SIZE] = *b"HTTP/1.1 ";
|
|
||||||
|
|
||||||
match version {
|
|
||||||
Version::HTTP_2 => buf[5] = b'2',
|
|
||||||
Version::HTTP_10 => buf[7] = b'0',
|
|
||||||
Version::HTTP_09 => {
|
|
||||||
buf[5] = b'0';
|
|
||||||
buf[7] = b'9';
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut curr: isize = 12;
|
|
||||||
let buf_ptr = buf.as_mut_ptr();
|
|
||||||
let lut_ptr = DEC_DIGITS_LUT.as_ptr();
|
|
||||||
let four = n > 999;
|
|
||||||
|
|
||||||
// decode 2 more chars, if > 2 chars
|
|
||||||
let d1 = (n % 100) << 1;
|
|
||||||
n /= 100;
|
|
||||||
curr -= 2;
|
|
||||||
unsafe {
|
|
||||||
ptr::copy_nonoverlapping(lut_ptr.offset(d1 as isize), buf_ptr.offset(curr), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// decode last 1 or 2 chars
|
|
||||||
if n < 10 {
|
|
||||||
curr -= 1;
|
|
||||||
unsafe {
|
|
||||||
*buf_ptr.offset(curr) = (n as u8) + b'0';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let d1 = n << 1;
|
|
||||||
curr -= 2;
|
|
||||||
unsafe {
|
|
||||||
ptr::copy_nonoverlapping(lut_ptr.offset(d1 as isize), buf_ptr.offset(curr), 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bytes.put_slice(&buf);
|
|
||||||
if four {
|
|
||||||
bytes.put_u8(b' ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,135 +0,0 @@
|
|||||||
use criterion::{criterion_group, criterion_main, Criterion};
|
|
||||||
|
|
||||||
use bytes::BytesMut;
|
|
||||||
|
|
||||||
// A Miri run detects UB, seen on this playground:
|
|
||||||
// https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=f5d9aa166aa48df8dca05fce2b6c3915
|
|
||||||
|
|
||||||
fn bench_header_parsing(c: &mut Criterion) {
|
|
||||||
c.bench_function("Original (Unsound) [short]", |b| {
|
|
||||||
b.iter(|| {
|
|
||||||
let mut buf = BytesMut::from(REQ_SHORT);
|
|
||||||
_original::parse_headers(&mut buf);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
c.bench_function("New (safe) [short]", |b| {
|
|
||||||
b.iter(|| {
|
|
||||||
let mut buf = BytesMut::from(REQ_SHORT);
|
|
||||||
_new::parse_headers(&mut buf);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
c.bench_function("Original (Unsound) [realistic]", |b| {
|
|
||||||
b.iter(|| {
|
|
||||||
let mut buf = BytesMut::from(REQ);
|
|
||||||
_original::parse_headers(&mut buf);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
c.bench_function("New (safe) [realistic]", |b| {
|
|
||||||
b.iter(|| {
|
|
||||||
let mut buf = BytesMut::from(REQ);
|
|
||||||
_new::parse_headers(&mut buf);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
criterion_group!(benches, bench_header_parsing);
|
|
||||||
criterion_main!(benches);
|
|
||||||
|
|
||||||
const MAX_HEADERS: usize = 96;
|
|
||||||
|
|
||||||
const EMPTY_HEADER_ARRAY: [httparse::Header<'static>; MAX_HEADERS] =
|
|
||||||
[httparse::EMPTY_HEADER; MAX_HEADERS];
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
struct HeaderIndex {
|
|
||||||
name: (usize, usize),
|
|
||||||
value: (usize, usize),
|
|
||||||
}
|
|
||||||
|
|
||||||
const EMPTY_HEADER_INDEX: HeaderIndex = HeaderIndex {
|
|
||||||
name: (0, 0),
|
|
||||||
value: (0, 0),
|
|
||||||
};
|
|
||||||
|
|
||||||
const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] = [EMPTY_HEADER_INDEX; MAX_HEADERS];
|
|
||||||
|
|
||||||
impl HeaderIndex {
|
|
||||||
fn record(bytes: &[u8], headers: &[httparse::Header<'_>], indices: &mut [HeaderIndex]) {
|
|
||||||
let bytes_ptr = bytes.as_ptr() as usize;
|
|
||||||
for (header, indices) in headers.iter().zip(indices.iter_mut()) {
|
|
||||||
let name_start = header.name.as_ptr() as usize - bytes_ptr;
|
|
||||||
let name_end = name_start + header.name.len();
|
|
||||||
indices.name = (name_start, name_end);
|
|
||||||
let value_start = header.value.as_ptr() as usize - bytes_ptr;
|
|
||||||
let value_end = value_start + header.value.len();
|
|
||||||
indices.value = (value_start, value_end);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// test cases taken from:
|
|
||||||
// https://github.com/seanmonstar/httparse/blob/master/benches/parse.rs
|
|
||||||
|
|
||||||
const REQ_SHORT: &[u8] = b"\
|
|
||||||
GET / HTTP/1.0\r\n\
|
|
||||||
Host: example.com\r\n\
|
|
||||||
Cookie: session=60; user_id=1\r\n\r\n";
|
|
||||||
|
|
||||||
const REQ: &[u8] = b"\
|
|
||||||
GET /wp-content/uploads/2010/03/hello-kitty-darth-vader-pink.jpg HTTP/1.1\r\n\
|
|
||||||
Host: www.kittyhell.com\r\n\
|
|
||||||
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; ja-JP-mac; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 Pathtraq/0.9\r\n\
|
|
||||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n\
|
|
||||||
Accept-Language: ja,en-us;q=0.7,en;q=0.3\r\n\
|
|
||||||
Accept-Encoding: gzip,deflate\r\n\
|
|
||||||
Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7\r\n\
|
|
||||||
Keep-Alive: 115\r\n\
|
|
||||||
Connection: keep-alive\r\n\
|
|
||||||
Cookie: wp_ozh_wsa_visits=2; wp_ozh_wsa_visit_lasttime=xxxxxxxxxx; __utma=xxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.x; __utmz=xxxxxxxxx.xxxxxxxxxx.x.x.utmccn=(referral)|utmcsr=reader.livedoor.com|utmcct=/reader/|utmcmd=referral|padding=under256\r\n\r\n";
|
|
||||||
|
|
||||||
mod _new {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
pub fn parse_headers(src: &mut BytesMut) -> usize {
|
|
||||||
let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY;
|
|
||||||
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY;
|
|
||||||
|
|
||||||
let mut req = httparse::Request::new(&mut parsed);
|
|
||||||
match req.parse(src).unwrap() {
|
|
||||||
httparse::Status::Complete(_len) => {
|
|
||||||
HeaderIndex::record(src, req.headers, &mut headers);
|
|
||||||
req.headers.len()
|
|
||||||
}
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod _original {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
use std::mem::MaybeUninit;
|
|
||||||
|
|
||||||
pub fn parse_headers(src: &mut BytesMut) -> usize {
|
|
||||||
#![allow(invalid_value, clippy::uninit_assumed_init)]
|
|
||||||
|
|
||||||
let mut headers: [HeaderIndex; MAX_HEADERS] =
|
|
||||||
unsafe { MaybeUninit::uninit().assume_init() };
|
|
||||||
|
|
||||||
#[allow(invalid_value)]
|
|
||||||
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] =
|
|
||||||
unsafe { MaybeUninit::uninit().assume_init() };
|
|
||||||
|
|
||||||
let mut req = httparse::Request::new(&mut parsed);
|
|
||||||
match req.parse(src).unwrap() {
|
|
||||||
httparse::Status::Complete(_len) => {
|
|
||||||
HeaderIndex::record(src, req.headers, &mut headers);
|
|
||||||
req.headers.len()
|
|
||||||
}
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,93 +0,0 @@
|
|||||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
|
|
||||||
|
|
||||||
fn bench_write_camel_case(c: &mut Criterion) {
|
|
||||||
let mut group = c.benchmark_group("write_camel_case");
|
|
||||||
|
|
||||||
let names = ["connection", "Transfer-Encoding", "transfer-encoding"];
|
|
||||||
|
|
||||||
for &i in &names {
|
|
||||||
let bts = i.as_bytes();
|
|
||||||
|
|
||||||
group.bench_with_input(BenchmarkId::new("Original", i), bts, |b, bts| {
|
|
||||||
b.iter(|| {
|
|
||||||
let mut buf = black_box([0; 24]);
|
|
||||||
_original::write_camel_case(black_box(bts), &mut buf)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group.bench_with_input(BenchmarkId::new("New", i), bts, |b, bts| {
|
|
||||||
b.iter(|| {
|
|
||||||
let mut buf = black_box([0; 24]);
|
|
||||||
let len = black_box(bts.len());
|
|
||||||
_new::write_camel_case(black_box(bts), buf.as_mut_ptr(), len)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
group.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
criterion_group!(benches, bench_write_camel_case);
|
|
||||||
criterion_main!(benches);
|
|
||||||
|
|
||||||
mod _new {
|
|
||||||
pub fn write_camel_case(value: &[u8], buf: *mut u8, len: usize) {
|
|
||||||
// first copy entire (potentially wrong) slice to output
|
|
||||||
let buffer = unsafe {
|
|
||||||
std::ptr::copy_nonoverlapping(value.as_ptr(), buf, len);
|
|
||||||
std::slice::from_raw_parts_mut(buf, len)
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut iter = value.iter();
|
|
||||||
|
|
||||||
// first character should be uppercase
|
|
||||||
if let Some(c @ b'a'..=b'z') = iter.next() {
|
|
||||||
buffer[0] = c & 0b1101_1111;
|
|
||||||
}
|
|
||||||
|
|
||||||
// track 1 ahead of the current position since that's the location being assigned to
|
|
||||||
let mut index = 2;
|
|
||||||
|
|
||||||
// remaining characters after hyphens should also be uppercase
|
|
||||||
while let Some(&c) = iter.next() {
|
|
||||||
if c == b'-' {
|
|
||||||
// advance iter by one and uppercase if needed
|
|
||||||
if let Some(c @ b'a'..=b'z') = iter.next() {
|
|
||||||
buffer[index] = c & 0b1101_1111;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
index += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod _original {
|
|
||||||
pub fn write_camel_case(value: &[u8], buffer: &mut [u8]) {
|
|
||||||
let mut index = 0;
|
|
||||||
let key = value;
|
|
||||||
let mut key_iter = key.iter();
|
|
||||||
|
|
||||||
if let Some(c) = key_iter.next() {
|
|
||||||
if *c >= b'a' && *c <= b'z' {
|
|
||||||
buffer[index] = *c ^ b' ';
|
|
||||||
index += 1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
while let Some(c) = key_iter.next() {
|
|
||||||
buffer[index] = *c;
|
|
||||||
index += 1;
|
|
||||||
if *c == b'-' {
|
|
||||||
if let Some(c) = key_iter.next() {
|
|
||||||
if *c >= b'a' && *c <= b'z' {
|
|
||||||
buffer[index] = *c ^ b' ';
|
|
||||||
index += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -22,4 +22,4 @@ pub(crate) use self::message_body::MessageBodyMapErr;
|
|||||||
pub use self::none::None;
|
pub use self::none::None;
|
||||||
pub use self::size::BodySize;
|
pub use self::size::BodySize;
|
||||||
pub use self::sized_stream::SizedStream;
|
pub use self::sized_stream::SizedStream;
|
||||||
pub use self::utils::to_bytes;
|
pub use self::utils::{to_bytes, to_bytes_limited, BodyLimitExceeded};
|
||||||
|
@ -3,75 +3,196 @@ use std::task::Poll;
|
|||||||
use actix_rt::pin;
|
use actix_rt::pin;
|
||||||
use actix_utils::future::poll_fn;
|
use actix_utils::future::poll_fn;
|
||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
|
use derive_more::{Display, Error};
|
||||||
use futures_core::ready;
|
use futures_core::ready;
|
||||||
|
|
||||||
use super::{BodySize, MessageBody};
|
use super::{BodySize, MessageBody};
|
||||||
|
|
||||||
/// Collects the body produced by a `MessageBody` implementation into `Bytes`.
|
/// Collects all the bytes produced by `body`.
|
||||||
///
|
///
|
||||||
/// Any errors produced by the body stream are returned immediately.
|
/// Any errors produced by the body stream are returned immediately.
|
||||||
///
|
///
|
||||||
|
/// Consider using [`to_bytes_limited`] instead to protect against memory exhaustion.
|
||||||
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_http::body::{self, to_bytes};
|
/// use actix_http::body::{self, to_bytes};
|
||||||
/// use bytes::Bytes;
|
/// use bytes::Bytes;
|
||||||
///
|
///
|
||||||
/// # async fn test_to_bytes() {
|
/// # actix_rt::System::new().block_on(async {
|
||||||
/// let body = body::None::new();
|
/// let body = body::None::new();
|
||||||
/// let bytes = to_bytes(body).await.unwrap();
|
/// let bytes = to_bytes(body).await.unwrap();
|
||||||
/// assert!(bytes.is_empty());
|
/// assert!(bytes.is_empty());
|
||||||
///
|
///
|
||||||
/// let body = Bytes::from_static(b"123");
|
/// let body = Bytes::from_static(b"123");
|
||||||
/// let bytes = to_bytes(body).await.unwrap();
|
/// let bytes = to_bytes(body).await.unwrap();
|
||||||
/// assert_eq!(bytes, b"123"[..]);
|
/// assert_eq!(bytes, "123");
|
||||||
/// # }
|
/// # });
|
||||||
/// ```
|
/// ```
|
||||||
pub async fn to_bytes<B: MessageBody>(body: B) -> Result<Bytes, B::Error> {
|
pub async fn to_bytes<B: MessageBody>(body: B) -> Result<Bytes, B::Error> {
|
||||||
|
to_bytes_limited(body, usize::MAX)
|
||||||
|
.await
|
||||||
|
.expect("body should never yield more than usize::MAX bytes")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Error type returned from [`to_bytes_limited`] when body produced exceeds limit.
|
||||||
|
#[derive(Debug, Display, Error)]
|
||||||
|
#[display(fmt = "limit exceeded while collecting body bytes")]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub struct BodyLimitExceeded;
|
||||||
|
|
||||||
|
/// Collects the bytes produced by `body`, up to `limit` bytes.
|
||||||
|
///
|
||||||
|
/// If a chunk read from `poll_next` causes the total number of bytes read to exceed `limit`, an
|
||||||
|
/// `Err(BodyLimitExceeded)` is returned.
|
||||||
|
///
|
||||||
|
/// Any errors produced by the body stream are returned immediately as `Ok(Err(B::Error))`.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use actix_http::body::{self, to_bytes_limited};
|
||||||
|
/// use bytes::Bytes;
|
||||||
|
///
|
||||||
|
/// # actix_rt::System::new().block_on(async {
|
||||||
|
/// let body = body::None::new();
|
||||||
|
/// let bytes = to_bytes_limited(body, 10).await.unwrap().unwrap();
|
||||||
|
/// assert!(bytes.is_empty());
|
||||||
|
///
|
||||||
|
/// let body = Bytes::from_static(b"123");
|
||||||
|
/// let bytes = to_bytes_limited(body, 10).await.unwrap().unwrap();
|
||||||
|
/// assert_eq!(bytes, "123");
|
||||||
|
///
|
||||||
|
/// let body = Bytes::from_static(b"123");
|
||||||
|
/// assert!(to_bytes_limited(body, 2).await.is_err());
|
||||||
|
/// # });
|
||||||
|
/// ```
|
||||||
|
pub async fn to_bytes_limited<B: MessageBody>(
|
||||||
|
body: B,
|
||||||
|
limit: usize,
|
||||||
|
) -> Result<Result<Bytes, B::Error>, BodyLimitExceeded> {
|
||||||
|
/// Sensible default (32kB) for initial, bounded allocation when collecting body bytes.
|
||||||
|
const INITIAL_ALLOC_BYTES: usize = 32 * 1024;
|
||||||
|
|
||||||
let cap = match body.size() {
|
let cap = match body.size() {
|
||||||
BodySize::None | BodySize::Sized(0) => return Ok(Bytes::new()),
|
BodySize::None | BodySize::Sized(0) => return Ok(Ok(Bytes::new())),
|
||||||
BodySize::Sized(size) => size as usize,
|
BodySize::Sized(size) if size as usize > limit => return Err(BodyLimitExceeded),
|
||||||
// good enough first guess for chunk size
|
BodySize::Sized(size) => (size as usize).min(INITIAL_ALLOC_BYTES),
|
||||||
BodySize::Stream => 32_768,
|
BodySize::Stream => INITIAL_ALLOC_BYTES,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut exceeded_limit = false;
|
||||||
let mut buf = BytesMut::with_capacity(cap);
|
let mut buf = BytesMut::with_capacity(cap);
|
||||||
|
|
||||||
pin!(body);
|
pin!(body);
|
||||||
|
|
||||||
poll_fn(|cx| loop {
|
match poll_fn(|cx| loop {
|
||||||
let body = body.as_mut();
|
let body = body.as_mut();
|
||||||
|
|
||||||
match ready!(body.poll_next(cx)) {
|
match ready!(body.poll_next(cx)) {
|
||||||
Some(Ok(bytes)) => buf.extend_from_slice(&bytes),
|
Some(Ok(bytes)) => {
|
||||||
|
// if limit is exceeded...
|
||||||
|
if buf.len() + bytes.len() > limit {
|
||||||
|
// ...set flag to true and break out of poll_fn
|
||||||
|
exceeded_limit = true;
|
||||||
|
return Poll::Ready(Ok(()));
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.extend_from_slice(&bytes)
|
||||||
|
}
|
||||||
None => return Poll::Ready(Ok(())),
|
None => return Poll::Ready(Ok(())),
|
||||||
Some(Err(err)) => return Poll::Ready(Err(err)),
|
Some(Err(err)) => return Poll::Ready(Err(err)),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await?;
|
.await
|
||||||
|
{
|
||||||
|
// propagate error returned from body poll
|
||||||
|
Err(err) => Ok(Err(err)),
|
||||||
|
|
||||||
Ok(buf.freeze())
|
// limit was exceeded while reading body
|
||||||
|
Ok(()) if exceeded_limit => Err(BodyLimitExceeded),
|
||||||
|
|
||||||
|
// otherwise return body buffer
|
||||||
|
Ok(()) => Ok(Ok(buf.freeze())),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod tests {
|
||||||
|
use std::io;
|
||||||
|
|
||||||
use futures_util::{stream, StreamExt as _};
|
use futures_util::{stream, StreamExt as _};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{body::BodyStream, Error};
|
use crate::{
|
||||||
|
body::{BodyStream, SizedStream},
|
||||||
|
Error,
|
||||||
|
};
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_to_bytes() {
|
async fn to_bytes_complete() {
|
||||||
let bytes = to_bytes(()).await.unwrap();
|
let bytes = to_bytes(()).await.unwrap();
|
||||||
assert!(bytes.is_empty());
|
assert!(bytes.is_empty());
|
||||||
|
|
||||||
let body = Bytes::from_static(b"123");
|
let body = Bytes::from_static(b"123");
|
||||||
let bytes = to_bytes(body).await.unwrap();
|
let bytes = to_bytes(body).await.unwrap();
|
||||||
assert_eq!(bytes, b"123"[..]);
|
assert_eq!(bytes, b"123"[..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn to_bytes_streams() {
|
||||||
let stream = stream::iter(vec![Bytes::from_static(b"123"), Bytes::from_static(b"abc")])
|
let stream = stream::iter(vec![Bytes::from_static(b"123"), Bytes::from_static(b"abc")])
|
||||||
.map(Ok::<_, Error>);
|
.map(Ok::<_, Error>);
|
||||||
let body = BodyStream::new(stream);
|
let body = BodyStream::new(stream);
|
||||||
let bytes = to_bytes(body).await.unwrap();
|
let bytes = to_bytes(body).await.unwrap();
|
||||||
assert_eq!(bytes, b"123abc"[..]);
|
assert_eq!(bytes, b"123abc"[..]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn to_bytes_limited_complete() {
|
||||||
|
let bytes = to_bytes_limited((), 0).await.unwrap().unwrap();
|
||||||
|
assert!(bytes.is_empty());
|
||||||
|
|
||||||
|
let bytes = to_bytes_limited((), 1).await.unwrap().unwrap();
|
||||||
|
assert!(bytes.is_empty());
|
||||||
|
|
||||||
|
assert!(to_bytes_limited(Bytes::from_static(b"12"), 0)
|
||||||
|
.await
|
||||||
|
.is_err());
|
||||||
|
assert!(to_bytes_limited(Bytes::from_static(b"12"), 1)
|
||||||
|
.await
|
||||||
|
.is_err());
|
||||||
|
assert!(to_bytes_limited(Bytes::from_static(b"12"), 2).await.is_ok());
|
||||||
|
assert!(to_bytes_limited(Bytes::from_static(b"12"), 3).await.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn to_bytes_limited_streams() {
|
||||||
|
// hinting a larger body fails
|
||||||
|
let body = SizedStream::new(8, stream::empty().map(Ok::<_, Error>));
|
||||||
|
assert!(to_bytes_limited(body, 3).await.is_err());
|
||||||
|
|
||||||
|
// hinting a smaller body is okay
|
||||||
|
let body = SizedStream::new(3, stream::empty().map(Ok::<_, Error>));
|
||||||
|
assert!(to_bytes_limited(body, 3).await.unwrap().unwrap().is_empty());
|
||||||
|
|
||||||
|
// hinting a smaller body then returning a larger one fails
|
||||||
|
let stream = stream::iter(vec![Bytes::from_static(b"1234")]).map(Ok::<_, Error>);
|
||||||
|
let body = SizedStream::new(3, stream);
|
||||||
|
assert!(to_bytes_limited(body, 3).await.is_err());
|
||||||
|
|
||||||
|
let stream = stream::iter(vec![Bytes::from_static(b"123"), Bytes::from_static(b"abc")])
|
||||||
|
.map(Ok::<_, Error>);
|
||||||
|
let body = BodyStream::new(stream);
|
||||||
|
assert!(to_bytes_limited(body, 3).await.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn to_body_limit_error() {
|
||||||
|
let err_stream = stream::once(async { Err(io::Error::new(io::ErrorKind::Other, "")) });
|
||||||
|
let body = SizedStream::new(8, err_stream);
|
||||||
|
// not too big, but propagates error from body stream
|
||||||
|
assert!(to_bytes_limited(body, 10).await.unwrap().is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -161,44 +161,44 @@ impl From<crate::ws::ProtocolError> for Error {
|
|||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum ParseError {
|
pub enum ParseError {
|
||||||
/// An invalid `Method`, such as `GE.T`.
|
/// An invalid `Method`, such as `GE.T`.
|
||||||
#[display(fmt = "Invalid Method specified")]
|
#[display(fmt = "invalid method specified")]
|
||||||
Method,
|
Method,
|
||||||
|
|
||||||
/// An invalid `Uri`, such as `exam ple.domain`.
|
/// An invalid `Uri`, such as `exam ple.domain`.
|
||||||
#[display(fmt = "Uri error: {}", _0)]
|
#[display(fmt = "URI error: {}", _0)]
|
||||||
Uri(InvalidUri),
|
Uri(InvalidUri),
|
||||||
|
|
||||||
/// An invalid `HttpVersion`, such as `HTP/1.1`
|
/// An invalid `HttpVersion`, such as `HTP/1.1`
|
||||||
#[display(fmt = "Invalid HTTP version specified")]
|
#[display(fmt = "invalid HTTP version specified")]
|
||||||
Version,
|
Version,
|
||||||
|
|
||||||
/// An invalid `Header`.
|
/// An invalid `Header`.
|
||||||
#[display(fmt = "Invalid Header provided")]
|
#[display(fmt = "invalid Header provided")]
|
||||||
Header,
|
Header,
|
||||||
|
|
||||||
/// A message head is too large to be reasonable.
|
/// A message head is too large to be reasonable.
|
||||||
#[display(fmt = "Message head is too large")]
|
#[display(fmt = "message head is too large")]
|
||||||
TooLarge,
|
TooLarge,
|
||||||
|
|
||||||
/// A message reached EOF, but is not complete.
|
/// A message reached EOF, but is not complete.
|
||||||
#[display(fmt = "Message is incomplete")]
|
#[display(fmt = "message is incomplete")]
|
||||||
Incomplete,
|
Incomplete,
|
||||||
|
|
||||||
/// An invalid `Status`, such as `1337 ELITE`.
|
/// An invalid `Status`, such as `1337 ELITE`.
|
||||||
#[display(fmt = "Invalid Status provided")]
|
#[display(fmt = "invalid status provided")]
|
||||||
Status,
|
Status,
|
||||||
|
|
||||||
/// A timeout occurred waiting for an IO event.
|
/// A timeout occurred waiting for an IO event.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[display(fmt = "Timeout")]
|
#[display(fmt = "timeout")]
|
||||||
Timeout,
|
Timeout,
|
||||||
|
|
||||||
/// An `io::Error` that occurred while trying to read or write to a network stream.
|
/// An I/O error that occurred while trying to read or write to a network stream.
|
||||||
#[display(fmt = "IO error: {}", _0)]
|
#[display(fmt = "I/O error: {}", _0)]
|
||||||
Io(io::Error),
|
Io(io::Error),
|
||||||
|
|
||||||
/// Parsing a field as string failed.
|
/// Parsing a field as string failed.
|
||||||
#[display(fmt = "UTF8 error: {}", _0)]
|
#[display(fmt = "UTF-8 error: {}", _0)]
|
||||||
Utf8(Utf8Error),
|
Utf8(Utf8Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -257,22 +257,19 @@ impl From<ParseError> for Response<BoxBody> {
|
|||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum PayloadError {
|
pub enum PayloadError {
|
||||||
/// A payload reached EOF, but is not complete.
|
/// A payload reached EOF, but is not complete.
|
||||||
#[display(
|
#[display(fmt = "payload reached EOF before completing: {:?}", _0)]
|
||||||
fmt = "A payload reached EOF, but is not complete. Inner error: {:?}",
|
|
||||||
_0
|
|
||||||
)]
|
|
||||||
Incomplete(Option<io::Error>),
|
Incomplete(Option<io::Error>),
|
||||||
|
|
||||||
/// Content encoding stream corruption.
|
/// Content encoding stream corruption.
|
||||||
#[display(fmt = "Can not decode content-encoding.")]
|
#[display(fmt = "can not decode content-encoding")]
|
||||||
EncodingCorrupted,
|
EncodingCorrupted,
|
||||||
|
|
||||||
/// Payload reached size limit.
|
/// Payload reached size limit.
|
||||||
#[display(fmt = "Payload reached size limit.")]
|
#[display(fmt = "payload reached size limit")]
|
||||||
Overflow,
|
Overflow,
|
||||||
|
|
||||||
/// Payload length is unknown.
|
/// Payload length is unknown.
|
||||||
#[display(fmt = "Payload length is unknown.")]
|
#[display(fmt = "payload length is unknown")]
|
||||||
UnknownLength,
|
UnknownLength,
|
||||||
|
|
||||||
/// HTTP/2 payload error.
|
/// HTTP/2 payload error.
|
||||||
@ -330,22 +327,23 @@ impl From<PayloadError> for Error {
|
|||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum DispatchError {
|
pub enum DispatchError {
|
||||||
/// Service error.
|
/// Service error.
|
||||||
#[display(fmt = "Service Error")]
|
#[display(fmt = "service error")]
|
||||||
Service(Response<BoxBody>),
|
Service(Response<BoxBody>),
|
||||||
|
|
||||||
/// Body streaming error.
|
/// Body streaming error.
|
||||||
#[display(fmt = "Body error: {}", _0)]
|
#[display(fmt = "body error: {}", _0)]
|
||||||
Body(Box<dyn StdError>),
|
Body(Box<dyn StdError>),
|
||||||
|
|
||||||
/// Upgrade service error.
|
/// Upgrade service error.
|
||||||
|
#[display(fmt = "upgrade error")]
|
||||||
Upgrade,
|
Upgrade,
|
||||||
|
|
||||||
/// An `io::Error` that occurred while trying to read or write to a network stream.
|
/// An `io::Error` that occurred while trying to read or write to a network stream.
|
||||||
#[display(fmt = "IO error: {}", _0)]
|
#[display(fmt = "I/O error: {}", _0)]
|
||||||
Io(io::Error),
|
Io(io::Error),
|
||||||
|
|
||||||
/// Request parse error.
|
/// Request parse error.
|
||||||
#[display(fmt = "Request parse error: {}", _0)]
|
#[display(fmt = "request parse error: {}", _0)]
|
||||||
Parse(ParseError),
|
Parse(ParseError),
|
||||||
|
|
||||||
/// HTTP/2 error.
|
/// HTTP/2 error.
|
||||||
@ -354,19 +352,19 @@ pub enum DispatchError {
|
|||||||
H2(h2::Error),
|
H2(h2::Error),
|
||||||
|
|
||||||
/// The first request did not complete within the specified timeout.
|
/// The first request did not complete within the specified timeout.
|
||||||
#[display(fmt = "The first request did not complete within the specified timeout")]
|
#[display(fmt = "request did not complete within the specified timeout")]
|
||||||
SlowRequestTimeout,
|
SlowRequestTimeout,
|
||||||
|
|
||||||
/// Disconnect timeout. Makes sense for ssl streams.
|
/// Disconnect timeout. Makes sense for TLS streams.
|
||||||
#[display(fmt = "Connection shutdown timeout")]
|
#[display(fmt = "connection shutdown timeout")]
|
||||||
DisconnectTimeout,
|
DisconnectTimeout,
|
||||||
|
|
||||||
/// Handler dropped payload before reading EOF.
|
/// Handler dropped payload before reading EOF.
|
||||||
#[display(fmt = "Handler dropped payload before reading EOF")]
|
#[display(fmt = "handler dropped payload before reading EOF")]
|
||||||
HandlerDroppedPayload,
|
HandlerDroppedPayload,
|
||||||
|
|
||||||
/// Internal error.
|
/// Internal error.
|
||||||
#[display(fmt = "Internal error")]
|
#[display(fmt = "internal error")]
|
||||||
InternalError,
|
InternalError,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -391,12 +389,12 @@ impl StdError for DispatchError {
|
|||||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum ContentTypeError {
|
pub enum ContentTypeError {
|
||||||
/// Can not parse content type
|
/// Can not parse content type.
|
||||||
#[display(fmt = "Can not parse content type")]
|
#[display(fmt = "could not parse content type")]
|
||||||
ParseError,
|
ParseError,
|
||||||
|
|
||||||
/// Unknown content encoding
|
/// Unknown content encoding.
|
||||||
#[display(fmt = "Unknown content encoding")]
|
#[display(fmt = "unknown content encoding")]
|
||||||
UnknownEncoding,
|
UnknownEncoding,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -424,7 +422,7 @@ mod tests {
|
|||||||
let err: Error = ParseError::Io(orig).into();
|
let err: Error = ParseError::Io(orig).into();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!("{}", err),
|
format!("{}", err),
|
||||||
"error parsing HTTP message: IO error: other"
|
"error parsing HTTP message: I/O error: other"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -451,7 +449,7 @@ mod tests {
|
|||||||
let err = PayloadError::Incomplete(None);
|
let err = PayloadError::Incomplete(None);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
"A payload reached EOF, but is not complete. Inner error: None"
|
"payload reached EOF before completing: None"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -471,7 +469,7 @@ mod tests {
|
|||||||
match ParseError::from($from) {
|
match ParseError::from($from) {
|
||||||
e @ $error => {
|
e @ $error => {
|
||||||
let desc = format!("{}", e);
|
let desc = format!("{}", e);
|
||||||
assert_eq!(desc, format!("IO error: {}", $from));
|
assert_eq!(desc, format!("I/O error: {}", $from));
|
||||||
}
|
}
|
||||||
_ => unreachable!("{:?}", $from),
|
_ => unreachable!("{:?}", $from),
|
||||||
}
|
}
|
||||||
|
@ -26,39 +26,39 @@ pub use self::proto::{hash_key, CloseCode, CloseReason, OpCode};
|
|||||||
#[derive(Debug, Display, Error, From)]
|
#[derive(Debug, Display, Error, From)]
|
||||||
pub enum ProtocolError {
|
pub enum ProtocolError {
|
||||||
/// Received an unmasked frame from client.
|
/// Received an unmasked frame from client.
|
||||||
#[display(fmt = "Received an unmasked frame from client.")]
|
#[display(fmt = "received an unmasked frame from client")]
|
||||||
UnmaskedFrame,
|
UnmaskedFrame,
|
||||||
|
|
||||||
/// Received a masked frame from server.
|
/// Received a masked frame from server.
|
||||||
#[display(fmt = "Received a masked frame from server.")]
|
#[display(fmt = "received a masked frame from server")]
|
||||||
MaskedFrame,
|
MaskedFrame,
|
||||||
|
|
||||||
/// Encountered invalid opcode.
|
/// Encountered invalid opcode.
|
||||||
#[display(fmt = "Invalid opcode: {}.", _0)]
|
#[display(fmt = "invalid opcode ({})", _0)]
|
||||||
InvalidOpcode(#[error(not(source))] u8),
|
InvalidOpcode(#[error(not(source))] u8),
|
||||||
|
|
||||||
/// Invalid control frame length
|
/// Invalid control frame length
|
||||||
#[display(fmt = "Invalid control frame length: {}.", _0)]
|
#[display(fmt = "invalid control frame length ({})", _0)]
|
||||||
InvalidLength(#[error(not(source))] usize),
|
InvalidLength(#[error(not(source))] usize),
|
||||||
|
|
||||||
/// Bad opcode.
|
/// Bad opcode.
|
||||||
#[display(fmt = "Bad opcode.")]
|
#[display(fmt = "bad opcode")]
|
||||||
BadOpCode,
|
BadOpCode,
|
||||||
|
|
||||||
/// A payload reached size limit.
|
/// A payload reached size limit.
|
||||||
#[display(fmt = "A payload reached size limit.")]
|
#[display(fmt = "payload reached size limit")]
|
||||||
Overflow,
|
Overflow,
|
||||||
|
|
||||||
/// Continuation is not started.
|
/// Continuation has not started.
|
||||||
#[display(fmt = "Continuation is not started.")]
|
#[display(fmt = "continuation has not started")]
|
||||||
ContinuationNotStarted,
|
ContinuationNotStarted,
|
||||||
|
|
||||||
/// Received new continuation but it is already started.
|
/// Received new continuation but it is already started.
|
||||||
#[display(fmt = "Received new continuation but it is already started.")]
|
#[display(fmt = "received new continuation but it has already started")]
|
||||||
ContinuationStarted,
|
ContinuationStarted,
|
||||||
|
|
||||||
/// Unknown continuation fragment.
|
/// Unknown continuation fragment.
|
||||||
#[display(fmt = "Unknown continuation fragment: {}.", _0)]
|
#[display(fmt = "unknown continuation fragment: {}", _0)]
|
||||||
ContinuationFragment(#[error(not(source))] OpCode),
|
ContinuationFragment(#[error(not(source))] OpCode),
|
||||||
|
|
||||||
/// I/O error.
|
/// I/O error.
|
||||||
@ -70,27 +70,27 @@ pub enum ProtocolError {
|
|||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Error)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Error)]
|
||||||
pub enum HandshakeError {
|
pub enum HandshakeError {
|
||||||
/// Only get method is allowed.
|
/// Only get method is allowed.
|
||||||
#[display(fmt = "Method not allowed.")]
|
#[display(fmt = "method not allowed")]
|
||||||
GetMethodRequired,
|
GetMethodRequired,
|
||||||
|
|
||||||
/// Upgrade header if not set to WebSocket.
|
/// Upgrade header if not set to WebSocket.
|
||||||
#[display(fmt = "WebSocket upgrade is expected.")]
|
#[display(fmt = "WebSocket upgrade is expected")]
|
||||||
NoWebsocketUpgrade,
|
NoWebsocketUpgrade,
|
||||||
|
|
||||||
/// Connection header is not set to upgrade.
|
/// Connection header is not set to upgrade.
|
||||||
#[display(fmt = "Connection upgrade is expected.")]
|
#[display(fmt = "connection upgrade is expected")]
|
||||||
NoConnectionUpgrade,
|
NoConnectionUpgrade,
|
||||||
|
|
||||||
/// WebSocket version header is not set.
|
/// WebSocket version header is not set.
|
||||||
#[display(fmt = "WebSocket version header is required.")]
|
#[display(fmt = "WebSocket version header is required")]
|
||||||
NoVersionHeader,
|
NoVersionHeader,
|
||||||
|
|
||||||
/// Unsupported WebSocket version.
|
/// Unsupported WebSocket version.
|
||||||
#[display(fmt = "Unsupported WebSocket version.")]
|
#[display(fmt = "unsupported WebSocket version")]
|
||||||
UnsupportedVersion,
|
UnsupportedVersion,
|
||||||
|
|
||||||
/// WebSocket key is not set or wrong.
|
/// WebSocket key is not set or wrong.
|
||||||
#[display(fmt = "Unknown websocket key.")]
|
#[display(fmt = "unknown WebSocket key")]
|
||||||
BadWebsocketKey,
|
BadWebsocketKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,13 +39,13 @@ impl WsService {
|
|||||||
|
|
||||||
#[derive(Debug, Display, Error, From)]
|
#[derive(Debug, Display, Error, From)]
|
||||||
enum WsServiceError {
|
enum WsServiceError {
|
||||||
#[display(fmt = "http error")]
|
#[display(fmt = "HTTP error")]
|
||||||
Http(actix_http::Error),
|
Http(actix_http::Error),
|
||||||
|
|
||||||
#[display(fmt = "ws handshake error")]
|
#[display(fmt = "WS handshake error")]
|
||||||
Ws(actix_http::ws::HandshakeError),
|
Ws(actix_http::ws::HandshakeError),
|
||||||
|
|
||||||
#[display(fmt = "io error")]
|
#[display(fmt = "I/O error")]
|
||||||
Io(std::io::Error),
|
Io(std::io::Error),
|
||||||
|
|
||||||
#[display(fmt = "dispatcher error")]
|
#[display(fmt = "dispatcher error")]
|
||||||
|
@ -2,10 +2,18 @@
|
|||||||
|
|
||||||
## Unreleased - 2023-xx-xx
|
## Unreleased - 2023-xx-xx
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
- 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.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Handler functions can now receive up to 16 extractor parameters.
|
||||||
|
|
||||||
## 4.3.1 - 2023-02-26
|
## 4.3.1 - 2023-02-26
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
- Add support for custom methods with the `#[route]` macro. [#2969]
|
- Add support for custom methods with the `#[route]` macro. [#2969]
|
||||||
|
|
||||||
[#2969]: https://github.com/actix/actix-web/pull/2969
|
[#2969]: https://github.com/actix/actix-web/pull/2969
|
||||||
|
@ -152,7 +152,7 @@ mod tests {
|
|||||||
let resp_err: &dyn ResponseError = &err;
|
let resp_err: &dyn ResponseError = &err;
|
||||||
|
|
||||||
let err = resp_err.downcast_ref::<PayloadError>().unwrap();
|
let err = resp_err.downcast_ref::<PayloadError>().unwrap();
|
||||||
assert_eq!(err.to_string(), "Payload reached size limit.");
|
assert_eq!(err.to_string(), "payload reached size limit");
|
||||||
|
|
||||||
let not_err = resp_err.downcast_ref::<ContentTypeError>();
|
let not_err = resp_err.downcast_ref::<ContentTypeError>();
|
||||||
assert!(not_err.is_none());
|
assert!(not_err.is_none());
|
||||||
|
@ -416,6 +416,10 @@ mod tuple_from_req {
|
|||||||
tuple_from_req! { TupleFromRequest10; A, B, C, D, E, F, G, H, I, J }
|
tuple_from_req! { TupleFromRequest10; A, B, C, D, E, F, G, H, I, J }
|
||||||
tuple_from_req! { TupleFromRequest11; A, B, C, D, E, F, G, H, I, J, K }
|
tuple_from_req! { TupleFromRequest11; A, B, C, D, E, F, G, H, I, J, K }
|
||||||
tuple_from_req! { TupleFromRequest12; A, B, C, D, E, F, G, H, I, J, K, L }
|
tuple_from_req! { TupleFromRequest12; A, B, C, D, E, F, G, H, I, J, K, L }
|
||||||
|
tuple_from_req! { TupleFromRequest13; A, B, C, D, E, F, G, H, I, J, K, L, M }
|
||||||
|
tuple_from_req! { TupleFromRequest14; A, B, C, D, E, F, G, H, I, J, K, L, M, N }
|
||||||
|
tuple_from_req! { TupleFromRequest15; A, B, C, D, E, F, G, H, I, J, K, L, M, N, O }
|
||||||
|
tuple_from_req! { TupleFromRequest16; A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -151,6 +151,10 @@ factory_tuple! { A B C D E F G H I }
|
|||||||
factory_tuple! { A B C D E F G H I J }
|
factory_tuple! { A B C D E F G H I J }
|
||||||
factory_tuple! { A B C D E F G H I J K }
|
factory_tuple! { A B C D E F G H I J K }
|
||||||
factory_tuple! { A B C D E F G H I J K L }
|
factory_tuple! { A B C D E F G H I J K L }
|
||||||
|
factory_tuple! { A B C D E F G H I J K L M }
|
||||||
|
factory_tuple! { A B C D E F G H I J K L M N }
|
||||||
|
factory_tuple! { A B C D E F G H I J K L M N O }
|
||||||
|
factory_tuple! { A B C D E F G H I J K L M N O P }
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
@ -167,6 +171,7 @@ mod tests {
|
|||||||
async fn handler_max(
|
async fn handler_max(
|
||||||
_01: (), _02: (), _03: (), _04: (), _05: (), _06: (),
|
_01: (), _02: (), _03: (), _04: (), _05: (), _06: (),
|
||||||
_07: (), _08: (), _09: (), _10: (), _11: (), _12: (),
|
_07: (), _08: (), _09: (), _10: (), _11: (), _12: (),
|
||||||
|
_13: (), _14: (), _15: (), _16: (),
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
assert_impl_handler(handler_min);
|
assert_impl_handler(handler_min);
|
||||||
|
@ -338,7 +338,7 @@ where
|
|||||||
/// # ; Ok(()) }
|
/// # ; Ok(()) }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn bind<A: net::ToSocketAddrs>(mut self, addrs: A) -> io::Result<Self> {
|
pub fn bind<A: net::ToSocketAddrs>(mut self, addrs: A) -> io::Result<Self> {
|
||||||
let sockets = self.bind2(addrs)?;
|
let sockets = bind_addrs(addrs, self.backlog)?;
|
||||||
|
|
||||||
for lst in sockets {
|
for lst in sockets {
|
||||||
self = self.listen(lst)?;
|
self = self.listen(lst)?;
|
||||||
@ -347,33 +347,6 @@ where
|
|||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bind2<A: net::ToSocketAddrs>(&self, addrs: A) -> io::Result<Vec<net::TcpListener>> {
|
|
||||||
let mut err = None;
|
|
||||||
let mut success = false;
|
|
||||||
let mut sockets = Vec::new();
|
|
||||||
|
|
||||||
for addr in addrs.to_socket_addrs()? {
|
|
||||||
match create_tcp_listener(addr, self.backlog) {
|
|
||||||
Ok(lst) => {
|
|
||||||
success = true;
|
|
||||||
sockets.push(lst);
|
|
||||||
}
|
|
||||||
Err(e) => err = Some(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if success {
|
|
||||||
Ok(sockets)
|
|
||||||
} else if let Some(e) = err.take() {
|
|
||||||
Err(e)
|
|
||||||
} else {
|
|
||||||
Err(io::Error::new(
|
|
||||||
io::ErrorKind::Other,
|
|
||||||
"Can not bind to address.",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolves socket address(es) and binds server to created listener(s) for TLS connections
|
/// Resolves socket address(es) and binds server to created listener(s) for TLS connections
|
||||||
/// using Rustls.
|
/// using Rustls.
|
||||||
///
|
///
|
||||||
@ -386,7 +359,7 @@ where
|
|||||||
addrs: A,
|
addrs: A,
|
||||||
config: RustlsServerConfig,
|
config: RustlsServerConfig,
|
||||||
) -> io::Result<Self> {
|
) -> io::Result<Self> {
|
||||||
let sockets = self.bind2(addrs)?;
|
let sockets = bind_addrs(addrs, self.backlog)?;
|
||||||
for lst in sockets {
|
for lst in sockets {
|
||||||
self = self.listen_rustls_inner(lst, config.clone())?;
|
self = self.listen_rustls_inner(lst, config.clone())?;
|
||||||
}
|
}
|
||||||
@ -404,7 +377,7 @@ where
|
|||||||
where
|
where
|
||||||
A: net::ToSocketAddrs,
|
A: net::ToSocketAddrs,
|
||||||
{
|
{
|
||||||
let sockets = self.bind2(addrs)?;
|
let sockets = bind_addrs(addrs, self.backlog)?;
|
||||||
let acceptor = openssl_acceptor(builder)?;
|
let acceptor = openssl_acceptor(builder)?;
|
||||||
|
|
||||||
for lst in sockets {
|
for lst in sockets {
|
||||||
@ -719,6 +692,38 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bind TCP listeners to socket addresses resolved from `addrs` with options.
|
||||||
|
fn bind_addrs(
|
||||||
|
addrs: impl net::ToSocketAddrs,
|
||||||
|
backlog: u32,
|
||||||
|
) -> io::Result<Vec<net::TcpListener>> {
|
||||||
|
let mut err = None;
|
||||||
|
let mut success = false;
|
||||||
|
let mut sockets = Vec::new();
|
||||||
|
|
||||||
|
for addr in addrs.to_socket_addrs()? {
|
||||||
|
match create_tcp_listener(addr, backlog) {
|
||||||
|
Ok(lst) => {
|
||||||
|
success = true;
|
||||||
|
sockets.push(lst);
|
||||||
|
}
|
||||||
|
Err(e) => err = Some(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if success {
|
||||||
|
Ok(sockets)
|
||||||
|
} else if let Some(err) = err.take() {
|
||||||
|
Err(err)
|
||||||
|
} else {
|
||||||
|
Err(io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
"Can not bind to address.",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a TCP listener from socket address and options.
|
||||||
fn create_tcp_listener(addr: net::SocketAddr, backlog: u32) -> io::Result<net::TcpListener> {
|
fn create_tcp_listener(addr: net::SocketAddr, backlog: u32) -> io::Result<net::TcpListener> {
|
||||||
use socket2::{Domain, Protocol, Socket, Type};
|
use socket2::{Domain, Protocol, Socket, Type};
|
||||||
let domain = Domain::for_address(addr);
|
let domain = Domain::for_address(addr);
|
||||||
@ -731,7 +736,7 @@ fn create_tcp_listener(addr: net::SocketAddr, backlog: u32) -> io::Result<net::T
|
|||||||
Ok(net::TcpListener::from(socket))
|
Ok(net::TcpListener::from(socket))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configure `SslAcceptorBuilder` with custom server flags.
|
/// Configures OpenSSL acceptor `builder` with ALPN protocols.
|
||||||
#[cfg(feature = "openssl")]
|
#[cfg(feature = "openssl")]
|
||||||
fn openssl_acceptor(mut builder: SslAcceptorBuilder) -> io::Result<SslAcceptor> {
|
fn openssl_acceptor(mut builder: SslAcceptorBuilder) -> io::Result<SslAcceptor> {
|
||||||
builder.set_alpn_select_callback(|_, protocols| {
|
builder.set_alpn_select_callback(|_, protocols| {
|
||||||
|
@ -304,7 +304,7 @@ mod tests {
|
|||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_either_extract_first_try() {
|
async fn test_either_extract_first_try() {
|
||||||
let (req, mut pl) = TestRequest::default()
|
let (req, mut pl) = TestRequest::default()
|
||||||
.set_form(&TestForm {
|
.set_form(TestForm {
|
||||||
hello: "world".to_owned(),
|
hello: "world".to_owned(),
|
||||||
})
|
})
|
||||||
.to_http_parts();
|
.to_http_parts();
|
||||||
@ -320,7 +320,7 @@ mod tests {
|
|||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_either_extract_fallback() {
|
async fn test_either_extract_fallback() {
|
||||||
let (req, mut pl) = TestRequest::default()
|
let (req, mut pl) = TestRequest::default()
|
||||||
.set_json(&TestForm {
|
.set_json(TestForm {
|
||||||
hello: "world".to_owned(),
|
hello: "world".to_owned(),
|
||||||
})
|
})
|
||||||
.to_http_parts();
|
.to_http_parts();
|
||||||
@ -351,7 +351,7 @@ mod tests {
|
|||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_either_extract_recursive_fallback_inner() {
|
async fn test_either_extract_recursive_fallback_inner() {
|
||||||
let (req, mut pl) = TestRequest::default()
|
let (req, mut pl) = TestRequest::default()
|
||||||
.set_json(&TestForm {
|
.set_json(TestForm {
|
||||||
hello: "world".to_owned(),
|
hello: "world".to_owned(),
|
||||||
})
|
})
|
||||||
.to_http_parts();
|
.to_http_parts();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user