mirror of
https://github.com/fafhrd91/actix-web
synced 2025-07-16 06:35:46 +02:00
Compare commits
18 Commits
multipart-
...
http-v3.9.
Author | SHA1 | Date | |
---|---|---|---|
|
9ba326aed0 | ||
|
882fb3d25b | ||
|
be28a0bd6d | ||
|
a431b7356c | ||
|
5be53820f0 | ||
|
d7d9000b19 | ||
|
e4e4bb799c | ||
|
323d1fa64f | ||
|
9aa62112aa | ||
|
270a6a3b70 | ||
|
07f720f716 | ||
|
f71f9ca66b | ||
|
b6bee346f7 | ||
|
5c6e0e17d3 | ||
|
e97e28db4f | ||
|
16125bd3be | ||
|
e9ccfbc866 | ||
|
e0e4d1e661 |
4
.github/workflows/ci-post-merge.yml
vendored
4
.github/workflows/ci-post-merge.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
toolchain: ${{ matrix.version.version }}
|
||||
|
||||
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
|
||||
uses: taiki-e/install-action@v2.41.7
|
||||
uses: taiki-e/install-action@v2.42.17
|
||||
with:
|
||||
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
|
||||
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
|
||||
|
||||
- name: Install just, cargo-hack
|
||||
uses: taiki-e/install-action@v2.41.7
|
||||
uses: taiki-e/install-action@v2.42.17
|
||||
with:
|
||||
tool: just,cargo-hack
|
||||
|
||||
|
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -64,7 +64,7 @@ jobs:
|
||||
toolchain: ${{ matrix.version.version }}
|
||||
|
||||
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
|
||||
uses: taiki-e/install-action@v2.41.7
|
||||
uses: taiki-e/install-action@v2.42.17
|
||||
with:
|
||||
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
|
||||
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
toolchain: nightly
|
||||
|
||||
- name: Install just
|
||||
uses: taiki-e/install-action@v2.41.7
|
||||
uses: taiki-e/install-action@v2.42.17
|
||||
with:
|
||||
tool: just
|
||||
|
||||
|
2
.github/workflows/coverage.yml
vendored
2
.github/workflows/coverage.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
components: llvm-tools
|
||||
|
||||
- name: Install just, cargo-llvm-cov, cargo-nextest
|
||||
uses: taiki-e/install-action@v2.41.7
|
||||
uses: taiki-e/install-action@v2.42.17
|
||||
with:
|
||||
tool: just,cargo-llvm-cov,cargo-nextest
|
||||
|
||||
|
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
@@ -76,7 +76,7 @@ jobs:
|
||||
toolchain: nightly-2024-05-01
|
||||
|
||||
- name: Install just
|
||||
uses: taiki-e/install-action@v2.41.7
|
||||
uses: taiki-e/install-action@v2.42.17
|
||||
with:
|
||||
tool: just
|
||||
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
toolchain: nightly-2024-06-07
|
||||
|
||||
- name: Install cargo-public-api
|
||||
uses: taiki-e/install-action@v2.41.7
|
||||
uses: taiki-e/install-action@v2.42.17
|
||||
with:
|
||||
tool: cargo-public-api
|
||||
|
||||
|
@@ -51,3 +51,11 @@ awc = { path = "awc" }
|
||||
# actix-utils = { path = "../actix-net/actix-utils" }
|
||||
# actix-tls = { path = "../actix-net/actix-tls" }
|
||||
# actix-server = { path = "../actix-net/actix-server" }
|
||||
|
||||
[workspace.lints.rust]
|
||||
rust_2018_idioms = { level = "deny" }
|
||||
future_incompatible = { level = "deny" }
|
||||
nonstandard_style = { level = "deny" }
|
||||
|
||||
[workspace.lints.clippy]
|
||||
# clone_on_ref_ptr = { level = "deny" }
|
||||
|
@@ -54,3 +54,6 @@ actix-test = "0.1"
|
||||
actix-web = "4"
|
||||
env_logger = "0.11"
|
||||
tempfile = "3.2"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
@@ -11,8 +11,7 @@
|
||||
//! .service(Files::new("/static", ".").prefer_utf8(true));
|
||||
//! ```
|
||||
|
||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||
#![warn(future_incompatible, missing_docs, missing_debug_implementations)]
|
||||
#![warn(missing_docs, missing_debug_implementations)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
|
@@ -59,3 +59,6 @@ tokio = { version = "1.24.2", features = ["sync"] }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-http = "3"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
@@ -1,7 +1,5 @@
|
||||
//! Various helpers for Actix applications to use during testing.
|
||||
|
||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||
#![warn(future_incompatible)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
|
@@ -2,6 +2,12 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
## 3.9.0
|
||||
|
||||
### Added
|
||||
|
||||
- Implement `FromIterator<(HeaderName, HeaderValue)>` for `HeaderMap`.
|
||||
|
||||
## 3.8.0
|
||||
|
||||
### Added
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-http"
|
||||
version = "3.8.0"
|
||||
version = "3.9.0"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Rob Ede <robjtede@icloud.com>",
|
||||
@@ -167,6 +167,9 @@ tls-openssl = { package = "openssl", version = "0.10.55" }
|
||||
tls-rustls_023 = { package = "rustls", version = "0.23" }
|
||||
tokio = { version = "1.24.2", features = ["net", "rt", "macros"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[[example]]
|
||||
name = "ws"
|
||||
required-features = ["ws", "rustls-0_23"]
|
||||
|
@@ -5,11 +5,11 @@
|
||||
<!-- prettier-ignore-start -->
|
||||
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://docs.rs/actix-http/3.8.0)
|
||||
[](https://docs.rs/actix-http/3.9.0)
|
||||

|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-http/3.8.0)
|
||||
[](https://deps.rs/crate/actix-http/3.9.0)
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
@@ -541,6 +541,6 @@ where
|
||||
|
||||
fn call(&self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future {
|
||||
let conn_data = OnConnectData::from_io(&io, self.on_connect_ext.as_deref());
|
||||
Dispatcher::new(io, self.flow.clone(), self.cfg.clone(), addr, conn_data)
|
||||
Dispatcher::new(io, Rc::clone(&self.flow), self.cfg.clone(), addr, conn_data)
|
||||
}
|
||||
}
|
||||
|
@@ -434,7 +434,7 @@ where
|
||||
|
||||
H2ServiceHandlerResponse {
|
||||
state: State::Handshake(
|
||||
Some(self.flow.clone()),
|
||||
Some(Rc::clone(&self.flow)),
|
||||
Some(self.cfg.clone()),
|
||||
addr,
|
||||
on_connect_data,
|
||||
|
@@ -13,8 +13,9 @@ use super::AsHeaderName;
|
||||
/// `HeaderMap` is a "multi-map" of [`HeaderName`] to one or more [`HeaderValue`]s.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use actix_http::header::{self, HeaderMap, HeaderValue};
|
||||
/// # use actix_http::header::{self, HeaderMap, HeaderValue};
|
||||
///
|
||||
/// let mut map = HeaderMap::new();
|
||||
///
|
||||
@@ -29,6 +30,21 @@ use super::AsHeaderName;
|
||||
///
|
||||
/// assert!(!map.contains_key(header::ORIGIN));
|
||||
/// ```
|
||||
///
|
||||
/// Construct a header map using the [`FromIterator`] implementation. Note that it uses the append
|
||||
/// strategy, so duplicate header names are preserved.
|
||||
///
|
||||
/// ```
|
||||
/// use actix_http::header::{self, HeaderMap, HeaderValue};
|
||||
///
|
||||
/// let headers = HeaderMap::from_iter([
|
||||
/// (header::CONTENT_TYPE, HeaderValue::from_static("text/plain")),
|
||||
/// (header::COOKIE, HeaderValue::from_static("foo=1")),
|
||||
/// (header::COOKIE, HeaderValue::from_static("bar=1")),
|
||||
/// ]);
|
||||
///
|
||||
/// assert_eq!(headers.len(), 3);
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct HeaderMap {
|
||||
pub(crate) inner: AHashMap<HeaderName, Value>,
|
||||
@@ -368,8 +384,8 @@ impl HeaderMap {
|
||||
/// let removed = map.insert(header::ACCEPT, HeaderValue::from_static("text/html"));
|
||||
/// assert!(!removed.is_empty());
|
||||
/// ```
|
||||
pub fn insert(&mut self, key: HeaderName, val: HeaderValue) -> Removed {
|
||||
let value = self.inner.insert(key, Value::one(val));
|
||||
pub fn insert(&mut self, name: HeaderName, val: HeaderValue) -> Removed {
|
||||
let value = self.inner.insert(name, Value::one(val));
|
||||
Removed::new(value)
|
||||
}
|
||||
|
||||
@@ -636,6 +652,16 @@ impl<'a> IntoIterator for &'a HeaderMap {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<(HeaderName, HeaderValue)> for HeaderMap {
|
||||
fn from_iter<T: IntoIterator<Item = (HeaderName, HeaderValue)>>(iter: T) -> Self {
|
||||
iter.into_iter()
|
||||
.fold(Self::new(), |mut map, (name, value)| {
|
||||
map.append(name, value);
|
||||
map
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a `http::HeaderMap` to our `HeaderMap`.
|
||||
impl From<http::HeaderMap> for HeaderMap {
|
||||
fn from(mut map: http::HeaderMap) -> Self {
|
||||
|
@@ -20,8 +20,6 @@
|
||||
//! [rustls]: https://crates.io/crates/rustls
|
||||
//! [trust-dns]: https://crates.io/crates/trust-dns
|
||||
|
||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||
#![warn(future_incompatible)]
|
||||
#![allow(
|
||||
clippy::type_complexity,
|
||||
clippy::too_many_arguments,
|
||||
|
@@ -66,7 +66,7 @@ impl<T: Head> ops::DerefMut for Message<T> {
|
||||
|
||||
impl<T: Head> Drop for Message<T> {
|
||||
fn drop(&mut self) {
|
||||
T::with_pool(|p| p.release(self.head.clone()))
|
||||
T::with_pool(|p| p.release(Rc::clone(&self.head)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -910,7 +910,7 @@ where
|
||||
handshake: Some((
|
||||
crate::h2::handshake_with_timeout(io, &self.cfg),
|
||||
self.cfg.clone(),
|
||||
self.flow.clone(),
|
||||
Rc::clone(&self.flow),
|
||||
conn_data,
|
||||
peer_addr,
|
||||
)),
|
||||
@@ -926,7 +926,7 @@ where
|
||||
state: State::H1 {
|
||||
dispatcher: h1::Dispatcher::new(
|
||||
io,
|
||||
self.flow.clone(),
|
||||
Rc::clone(&self.flow),
|
||||
self.cfg.clone(),
|
||||
peer_addr,
|
||||
conn_data,
|
||||
|
@@ -159,8 +159,8 @@ impl TestBuffer {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn clone(&self) -> Self {
|
||||
Self {
|
||||
read_buf: self.read_buf.clone(),
|
||||
write_buf: self.write_buf.clone(),
|
||||
read_buf: Rc::clone(&self.read_buf),
|
||||
write_buf: Rc::clone(&self.write_buf),
|
||||
err: self.err.clone(),
|
||||
}
|
||||
}
|
||||
|
@@ -29,3 +29,6 @@ actix-multipart = "0.7"
|
||||
actix-web = "4"
|
||||
rustversion = "1"
|
||||
trybuild = "1"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
@@ -2,8 +2,6 @@
|
||||
//!
|
||||
//! See [`macro@MultipartForm`] for usage examples.
|
||||
|
||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||
#![warn(future_incompatible)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
|
@@ -4,13 +4,14 @@ version = "0.7.2"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Jacob Halsey <jacob@jhalsey.com>",
|
||||
"Rob Ede <robjtede@icloud.com>",
|
||||
]
|
||||
description = "Multipart form support for Actix Web"
|
||||
keywords = ["http", "web", "framework", "async", "futures"]
|
||||
homepage = "https://actix.rs"
|
||||
repository = "https://github.com/actix/actix-web"
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2021"
|
||||
description = "Multipart request & form support for Actix Web"
|
||||
keywords = ["http", "actix", "web", "multipart", "form"]
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
@@ -71,7 +72,5 @@ multer = "3"
|
||||
tokio = { version = "1.24.2", features = ["sync"] }
|
||||
tokio-stream = "0.1"
|
||||
|
||||
[lints.rust]
|
||||
future_incompatible = { level = "deny" }
|
||||
rust_2018_idioms = { level = "deny" }
|
||||
nonstandard_style = { level = "deny" }
|
||||
[lints]
|
||||
workspace = true
|
||||
|
@@ -15,14 +15,18 @@
|
||||
|
||||
<!-- cargo-rdme start -->
|
||||
|
||||
Multipart form support for Actix Web.
|
||||
Multipart request & form support for Actix Web.
|
||||
|
||||
The [`Multipart`] extractor aims to support all kinds of `multipart/*` requests, including `multipart/form-data`, `multipart/related` and `multipart/mixed`. This is a lower-level extractor which supports reading [multipart fields](Field), in the order they are sent by the client.
|
||||
|
||||
Due to additional requirements for `multipart/form-data` requests, the higher level [`MultipartForm`] extractor and derive macro only supports this media type.
|
||||
|
||||
## Examples
|
||||
|
||||
```rust
|
||||
use actix_web::{post, App, HttpServer, Responder};
|
||||
|
||||
use actix_multipart::form::{json::Json as MPJson, tempfile::TempFile, MultipartForm};
|
||||
use actix_multipart::form::{json::Json as MpJson, tempfile::TempFile, MultipartForm};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -34,7 +38,7 @@ struct Metadata {
|
||||
struct UploadForm {
|
||||
#[multipart(limit = "100MB")]
|
||||
file: TempFile,
|
||||
json: MPJson<Metadata>,
|
||||
json: MpJson<Metadata>,
|
||||
}
|
||||
|
||||
#[post("/videos")]
|
||||
@@ -63,6 +67,8 @@ curl -v --request POST \
|
||||
-F file=@./Cargo.lock
|
||||
```
|
||||
|
||||
[`MultipartForm`]: struct@form::MultipartForm
|
||||
|
||||
<!-- cargo-rdme end -->
|
||||
|
||||
[More available in the examples repo →](https://github.com/actix/examples/tree/master/forms/multipart)
|
||||
|
@@ -209,8 +209,14 @@ impl fmt::Debug for Field {
|
||||
pub(crate) struct InnerField {
|
||||
/// Payload is initialized as Some and is `take`n when the field stream finishes.
|
||||
payload: Option<PayloadRef>,
|
||||
|
||||
/// Field boundary (without "--" prefix).
|
||||
boundary: String,
|
||||
|
||||
/// True if request payload has been exhausted.
|
||||
eof: bool,
|
||||
|
||||
/// Field data's stated size according to it's Content-Length header.
|
||||
length: Option<u64>,
|
||||
}
|
||||
|
||||
@@ -286,6 +292,7 @@ impl InnerField {
|
||||
let mut pos = 0;
|
||||
|
||||
let len = payload.buf.len();
|
||||
|
||||
if len == 0 {
|
||||
return if payload.eof {
|
||||
Poll::Ready(Some(Err(Error::Incomplete)))
|
||||
@@ -296,7 +303,7 @@ impl InnerField {
|
||||
|
||||
// check boundary
|
||||
if len > 4 && payload.buf[0] == b'\r' {
|
||||
let b_len = if &payload.buf[..2] == b"\r\n" && &payload.buf[2..4] == b"--" {
|
||||
let b_len = if payload.buf.starts_with(b"\r\n") && &payload.buf[2..4] == b"--" {
|
||||
Some(4)
|
||||
} else if &payload.buf[1..3] == b"--" {
|
||||
Some(3)
|
||||
@@ -357,41 +364,42 @@ impl InnerField {
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
|
||||
let result = if let Some(mut payload) = self
|
||||
let Some(mut payload) = self
|
||||
.payload
|
||||
.as_ref()
|
||||
.expect("Field should not be polled after completion")
|
||||
.get_mut(safety)
|
||||
{
|
||||
if !self.eof {
|
||||
let res = if let Some(ref mut len) = self.length {
|
||||
InnerField::read_len(&mut payload, len)
|
||||
} else {
|
||||
InnerField::read_stream(&mut payload, &self.boundary)
|
||||
};
|
||||
|
||||
match res {
|
||||
Poll::Pending => return Poll::Pending,
|
||||
Poll::Ready(Some(Ok(bytes))) => return Poll::Ready(Some(Ok(bytes))),
|
||||
Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err))),
|
||||
Poll::Ready(None) => self.eof = true,
|
||||
}
|
||||
}
|
||||
|
||||
match payload.readline() {
|
||||
Ok(None) => Poll::Pending,
|
||||
Ok(Some(line)) => {
|
||||
if line.as_ref() != b"\r\n" {
|
||||
log::warn!("multipart field did not read all the data or it is malformed");
|
||||
}
|
||||
Poll::Ready(None)
|
||||
}
|
||||
Err(err) => Poll::Ready(Some(Err(err))),
|
||||
}
|
||||
} else {
|
||||
Poll::Pending
|
||||
else {
|
||||
return Poll::Pending;
|
||||
};
|
||||
|
||||
if !self.eof {
|
||||
let res = if let Some(ref mut len) = self.length {
|
||||
Self::read_len(&mut payload, len)
|
||||
} else {
|
||||
Self::read_stream(&mut payload, &self.boundary)
|
||||
};
|
||||
|
||||
match ready!(res) {
|
||||
Some(Ok(bytes)) => return Poll::Ready(Some(Ok(bytes))),
|
||||
Some(Err(err)) => return Poll::Ready(Some(Err(err))),
|
||||
None => self.eof = true,
|
||||
}
|
||||
}
|
||||
|
||||
let result = match payload.readline() {
|
||||
Ok(None) => Poll::Pending,
|
||||
Ok(Some(line)) => {
|
||||
if line.as_ref() != b"\r\n" {
|
||||
log::warn!("multipart field did not read all the data or it is malformed");
|
||||
}
|
||||
Poll::Ready(None)
|
||||
}
|
||||
Err(err) => Poll::Ready(Some(Err(err))),
|
||||
};
|
||||
|
||||
drop(payload);
|
||||
|
||||
if let Poll::Ready(None) = result {
|
||||
// drop payload buffer and make future un-poll-able
|
||||
let _ = self.payload.take();
|
||||
|
@@ -1,4 +1,4 @@
|
||||
//! Process and extract typed data from a multipart stream.
|
||||
//! Extract and process typed data from fields of a `multipart/form-data` request.
|
||||
|
||||
use std::{
|
||||
any::Any,
|
||||
|
@@ -1,4 +1,12 @@
|
||||
//! Multipart form support for Actix Web.
|
||||
//! Multipart request & form support for Actix Web.
|
||||
//!
|
||||
//! The [`Multipart`] extractor aims to support all kinds of `multipart/*` requests, including
|
||||
//! `multipart/form-data`, `multipart/related` and `multipart/mixed`. This is a lower-level
|
||||
//! extractor which supports reading [multipart fields](Field), in the order they are sent by the
|
||||
//! client.
|
||||
//!
|
||||
//! Due to additional requirements for `multipart/form-data` requests, the higher level
|
||||
//! [`MultipartForm`] extractor and derive macro only supports this media type.
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
@@ -45,6 +53,8 @@
|
||||
//! -F 'json={"name": "Cargo.lock"};type=application/json' \
|
||||
//! -F file=@./Cargo.lock
|
||||
//! ```
|
||||
//!
|
||||
//! [`MultipartForm`]: struct@form::MultipartForm
|
||||
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
|
@@ -483,7 +483,7 @@ mod tests {
|
||||
};
|
||||
use assert_matches::assert_matches;
|
||||
use futures_test::stream::StreamTestExt as _;
|
||||
use futures_util::{future::lazy, stream, StreamExt as _};
|
||||
use futures_util::{stream, StreamExt as _};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
|
||||
@@ -718,100 +718,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_basic() {
|
||||
let (_, payload) = h1::Payload::create(false);
|
||||
let mut payload = PayloadBuffer::new(payload);
|
||||
|
||||
assert_eq!(payload.buf.len(), 0);
|
||||
lazy(|cx| payload.poll_stream(cx)).await.unwrap();
|
||||
assert_eq!(None, payload.read_max(1).unwrap());
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_eof() {
|
||||
let (mut sender, payload) = h1::Payload::create(false);
|
||||
let mut payload = PayloadBuffer::new(payload);
|
||||
|
||||
assert_eq!(None, payload.read_max(4).unwrap());
|
||||
sender.feed_data(Bytes::from("data"));
|
||||
sender.feed_eof();
|
||||
lazy(|cx| payload.poll_stream(cx)).await.unwrap();
|
||||
|
||||
assert_eq!(Some(Bytes::from("data")), payload.read_max(4).unwrap());
|
||||
assert_eq!(payload.buf.len(), 0);
|
||||
assert!(payload.read_max(1).is_err());
|
||||
assert!(payload.eof);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_err() {
|
||||
let (mut sender, payload) = h1::Payload::create(false);
|
||||
let mut payload = PayloadBuffer::new(payload);
|
||||
assert_eq!(None, payload.read_max(1).unwrap());
|
||||
sender.set_error(PayloadError::Incomplete(None));
|
||||
lazy(|cx| payload.poll_stream(cx)).await.err().unwrap();
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn read_max() {
|
||||
let (mut sender, payload) = h1::Payload::create(false);
|
||||
let mut payload = PayloadBuffer::new(payload);
|
||||
|
||||
sender.feed_data(Bytes::from("line1"));
|
||||
sender.feed_data(Bytes::from("line2"));
|
||||
lazy(|cx| payload.poll_stream(cx)).await.unwrap();
|
||||
assert_eq!(payload.buf.len(), 10);
|
||||
|
||||
assert_eq!(Some(Bytes::from("line1")), payload.read_max(5).unwrap());
|
||||
assert_eq!(payload.buf.len(), 5);
|
||||
|
||||
assert_eq!(Some(Bytes::from("line2")), payload.read_max(5).unwrap());
|
||||
assert_eq!(payload.buf.len(), 0);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn read_exactly() {
|
||||
let (mut sender, payload) = h1::Payload::create(false);
|
||||
let mut payload = PayloadBuffer::new(payload);
|
||||
|
||||
assert_eq!(None, payload.read_exact(2));
|
||||
|
||||
sender.feed_data(Bytes::from("line1"));
|
||||
sender.feed_data(Bytes::from("line2"));
|
||||
lazy(|cx| payload.poll_stream(cx)).await.unwrap();
|
||||
|
||||
assert_eq!(Some(Bytes::from_static(b"li")), payload.read_exact(2));
|
||||
assert_eq!(payload.buf.len(), 8);
|
||||
|
||||
assert_eq!(Some(Bytes::from_static(b"ne1l")), payload.read_exact(4));
|
||||
assert_eq!(payload.buf.len(), 4);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn read_until() {
|
||||
let (mut sender, payload) = h1::Payload::create(false);
|
||||
let mut payload = PayloadBuffer::new(payload);
|
||||
|
||||
assert_eq!(None, payload.read_until(b"ne").unwrap());
|
||||
|
||||
sender.feed_data(Bytes::from("line1"));
|
||||
sender.feed_data(Bytes::from("line2"));
|
||||
lazy(|cx| payload.poll_stream(cx)).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Some(Bytes::from("line")),
|
||||
payload.read_until(b"ne").unwrap()
|
||||
);
|
||||
assert_eq!(payload.buf.len(), 6);
|
||||
|
||||
assert_eq!(
|
||||
Some(Bytes::from("1line2")),
|
||||
payload.read_until(b"2").unwrap()
|
||||
);
|
||||
assert_eq!(payload.buf.len(), 0);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_multipart_from_error() {
|
||||
let err = Error::ContentTypeMissing;
|
||||
|
@@ -122,7 +122,13 @@ impl PayloadBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads bytes until new line delimiter.
|
||||
/// Reads bytes until new line delimiter (`\n`, `0x0A`).
|
||||
///
|
||||
/// Returns:
|
||||
///
|
||||
/// - `Ok(Some(chunk))` - `needle` is found, with chunk ending after needle
|
||||
/// - `Err(Incomplete)` - `needle` is not found and we're at EOF
|
||||
/// - `Ok(None)` - `needle` is not found otherwise
|
||||
#[inline]
|
||||
pub(crate) fn readline(&mut self) -> Result<Option<Bytes>, Error> {
|
||||
self.read_until(b"\n")
|
||||
@@ -145,3 +151,105 @@ impl PayloadBuffer {
|
||||
self.buf.extend_from_slice(&buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use actix_http::h1;
|
||||
use futures_util::future::lazy;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn basic() {
|
||||
let (_, payload) = h1::Payload::create(false);
|
||||
let mut payload = PayloadBuffer::new(payload);
|
||||
|
||||
assert_eq!(payload.buf.len(), 0);
|
||||
lazy(|cx| payload.poll_stream(cx)).await.unwrap();
|
||||
assert_eq!(None, payload.read_max(1).unwrap());
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn eof() {
|
||||
let (mut sender, payload) = h1::Payload::create(false);
|
||||
let mut payload = PayloadBuffer::new(payload);
|
||||
|
||||
assert_eq!(None, payload.read_max(4).unwrap());
|
||||
sender.feed_data(Bytes::from("data"));
|
||||
sender.feed_eof();
|
||||
lazy(|cx| payload.poll_stream(cx)).await.unwrap();
|
||||
|
||||
assert_eq!(Some(Bytes::from("data")), payload.read_max(4).unwrap());
|
||||
assert_eq!(payload.buf.len(), 0);
|
||||
assert!(payload.read_max(1).is_err());
|
||||
assert!(payload.eof);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn err() {
|
||||
let (mut sender, payload) = h1::Payload::create(false);
|
||||
let mut payload = PayloadBuffer::new(payload);
|
||||
assert_eq!(None, payload.read_max(1).unwrap());
|
||||
sender.set_error(PayloadError::Incomplete(None));
|
||||
lazy(|cx| payload.poll_stream(cx)).await.err().unwrap();
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn read_max() {
|
||||
let (mut sender, payload) = h1::Payload::create(false);
|
||||
let mut payload = PayloadBuffer::new(payload);
|
||||
|
||||
sender.feed_data(Bytes::from("line1"));
|
||||
sender.feed_data(Bytes::from("line2"));
|
||||
lazy(|cx| payload.poll_stream(cx)).await.unwrap();
|
||||
assert_eq!(payload.buf.len(), 10);
|
||||
|
||||
assert_eq!(Some(Bytes::from("line1")), payload.read_max(5).unwrap());
|
||||
assert_eq!(payload.buf.len(), 5);
|
||||
|
||||
assert_eq!(Some(Bytes::from("line2")), payload.read_max(5).unwrap());
|
||||
assert_eq!(payload.buf.len(), 0);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn read_exactly() {
|
||||
let (mut sender, payload) = h1::Payload::create(false);
|
||||
let mut payload = PayloadBuffer::new(payload);
|
||||
|
||||
assert_eq!(None, payload.read_exact(2));
|
||||
|
||||
sender.feed_data(Bytes::from("line1"));
|
||||
sender.feed_data(Bytes::from("line2"));
|
||||
lazy(|cx| payload.poll_stream(cx)).await.unwrap();
|
||||
|
||||
assert_eq!(Some(Bytes::from_static(b"li")), payload.read_exact(2));
|
||||
assert_eq!(payload.buf.len(), 8);
|
||||
|
||||
assert_eq!(Some(Bytes::from_static(b"ne1l")), payload.read_exact(4));
|
||||
assert_eq!(payload.buf.len(), 4);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn read_until() {
|
||||
let (mut sender, payload) = h1::Payload::create(false);
|
||||
let mut payload = PayloadBuffer::new(payload);
|
||||
|
||||
assert_eq!(None, payload.read_until(b"ne").unwrap());
|
||||
|
||||
sender.feed_data(Bytes::from("line1"));
|
||||
sender.feed_data(Bytes::from("line2"));
|
||||
lazy(|cx| payload.poll_stream(cx)).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Some(Bytes::from("line")),
|
||||
payload.read_until(b"ne").unwrap()
|
||||
);
|
||||
assert_eq!(payload.buf.len(), 6);
|
||||
|
||||
assert_eq!(
|
||||
Some(Bytes::from("1line2")),
|
||||
payload.read_until(b"2").unwrap()
|
||||
);
|
||||
assert_eq!(payload.buf.len(), 0);
|
||||
}
|
||||
}
|
||||
|
@@ -38,6 +38,9 @@ http = "0.2.7"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
percent-encoding = "2.1"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[[bench]]
|
||||
name = "router"
|
||||
harness = false
|
||||
|
@@ -511,11 +511,6 @@ mod tests {
|
||||
value: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Id {
|
||||
_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Test1(String, u32);
|
||||
|
||||
|
@@ -1,7 +1,5 @@
|
||||
//! Resource path matching and router.
|
||||
|
||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||
#![warn(future_incompatible)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
|
@@ -73,3 +73,6 @@ tls-rustls-0_21 = { package = "rustls", version = "0.21", optional = true }
|
||||
tls-rustls-0_22 = { package = "rustls", version = "0.22", optional = true }
|
||||
tls-rustls-0_23 = { package = "rustls", version = "0.23", default-features = false, optional = true }
|
||||
tokio = { version = "1.24.2", features = ["sync"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
@@ -27,8 +27,6 @@
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||
#![warn(future_incompatible)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
|
@@ -2,7 +2,10 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Take the encoded buffer when yielding bytes in the response stream rather than splitting the buffer, reducing memory use
|
||||
## 4.3.1 <!-- v4.3.1+deprecated -->
|
||||
|
||||
- Reduce memory usage by `take`-ing (rather than `split`-ing) the encoded buffer when yielding bytes in the response stream.
|
||||
- Mark crate as deprecated.
|
||||
- Minimum supported Rust version (MSRV) is now 1.72.
|
||||
|
||||
## 4.3.0
|
||||
|
@@ -1,13 +1,14 @@
|
||||
[package]
|
||||
name = "actix-web-actors"
|
||||
version = "4.3.0"
|
||||
version = "4.3.1+deprecated"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Actix actors support for Actix Web"
|
||||
keywords = ["actix", "http", "web", "framework", "async"]
|
||||
homepage = "https://actix.rs"
|
||||
repository = "https://github.com/actix/actix-web"
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2021"
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[package.metadata.cargo_check_external_types]
|
||||
allowed_external_types = [
|
||||
@@ -41,3 +42,6 @@ actix-web = { version = "4", features = ["macros"] }
|
||||
env_logger = "0.11"
|
||||
futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
|
||||
mime = "0.3"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
@@ -1,15 +1,17 @@
|
||||
# `actix-web-actors`
|
||||
|
||||
> Actix actors support for Actix Web.
|
||||
>
|
||||
> This crate is deprecated. Migrate to [`actix-ws`](https://crates.io/crates/actix-ws).
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
|
||||
[](https://crates.io/crates/actix-web-actors)
|
||||
[](https://docs.rs/actix-web-actors/4.3.0)
|
||||
[](https://docs.rs/actix-web-actors/4.3.1)
|
||||

|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-web-actors/4.3.0)
|
||||

|
||||
[](https://crates.io/crates/actix-web-actors)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
@@ -1,5 +1,7 @@
|
||||
//! Actix actors support for Actix Web.
|
||||
//!
|
||||
//! This crate is deprecated. Migrate to [`actix-ws`](https://crates.io/crates/actix-ws).
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ```no_run
|
||||
@@ -55,8 +57,6 @@
|
||||
//! * [`HttpContext`]: This struct provides actor support for streaming HTTP responses.
|
||||
//!
|
||||
|
||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||
#![warn(future_incompatible)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
|
@@ -35,3 +35,6 @@ actix-web = "4"
|
||||
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
||||
trybuild = "1"
|
||||
rustversion = "1"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
@@ -73,8 +73,6 @@
|
||||
//! [DELETE]: macro@delete
|
||||
|
||||
#![recursion_limit = "512"]
|
||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||
#![warn(future_incompatible)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
|
@@ -145,7 +145,7 @@ async fn custom_resource_name_test<'a>(req: HttpRequest) -> impl Responder {
|
||||
mod guard_module {
|
||||
use actix_web::{guard::GuardContext, http::header};
|
||||
|
||||
pub fn guard(ctx: &GuardContext) -> bool {
|
||||
pub fn guard(ctx: &GuardContext<'_>) -> bool {
|
||||
ctx.header::<header::Accept>()
|
||||
.map(|h| h.preference() == "image/*")
|
||||
.unwrap_or(false)
|
||||
|
@@ -1,7 +1,7 @@
|
||||
use actix_web::{guard::GuardContext, http, http::header, web, App, HttpResponse, Responder};
|
||||
use actix_web_codegen::{delete, get, post, route, routes, scope};
|
||||
|
||||
pub fn image_guard(ctx: &GuardContext) -> bool {
|
||||
pub fn image_guard(ctx: &GuardContext<'_>) -> bool {
|
||||
ctx.header::<header::Accept>()
|
||||
.map(|h| h.preference() == "image/*")
|
||||
.unwrap_or(false)
|
||||
|
@@ -2,6 +2,11 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- Add `middleware::from_fn()` helper.
|
||||
- Add `web::ThinData` extractor.
|
||||
|
||||
## 4.8.0
|
||||
|
||||
### Added
|
||||
|
@@ -151,6 +151,7 @@ encoding_rs = "0.8"
|
||||
futures-core = { version = "0.3.17", default-features = false }
|
||||
futures-util = { version = "0.3.17", default-features = false }
|
||||
itoa = "1"
|
||||
impl-more = "0.1.4"
|
||||
language-tags = "0.3"
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
@@ -188,6 +189,9 @@ tls-rustls = { package = "rustls", version = "0.23" }
|
||||
tokio = { version = "1.24.2", features = ["rt-multi-thread", "macros"] }
|
||||
zstd = "0.13"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[[test]]
|
||||
name = "test_server"
|
||||
required-features = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"]
|
||||
|
@@ -2,11 +2,9 @@ use std::{future::Future, time::Instant};
|
||||
|
||||
use actix_http::body::BoxBody;
|
||||
use actix_utils::future::{ready, Ready};
|
||||
use actix_web::{
|
||||
error, http::StatusCode, test::TestRequest, Error, HttpRequest, HttpResponse, Responder,
|
||||
};
|
||||
use actix_web::{http::StatusCode, test::TestRequest, Error, HttpRequest, HttpResponse, Responder};
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use futures_util::future::{join_all, Either};
|
||||
use futures_util::future::join_all;
|
||||
|
||||
// responder simulate the old responder trait.
|
||||
trait FutureResponder {
|
||||
@@ -16,9 +14,6 @@ trait FutureResponder {
|
||||
fn future_respond_to(self, req: &HttpRequest) -> Self::Future;
|
||||
}
|
||||
|
||||
// a simple option responder type.
|
||||
struct OptionResponder<T>(Option<T>);
|
||||
|
||||
// a simple wrapper type around string
|
||||
struct StringResponder(String);
|
||||
|
||||
@@ -34,22 +29,6 @@ impl FutureResponder for StringResponder {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> FutureResponder for OptionResponder<T>
|
||||
where
|
||||
T: FutureResponder,
|
||||
T::Future: Future<Output = Result<HttpResponse, Error>>,
|
||||
{
|
||||
type Error = Error;
|
||||
type Future = Either<T::Future, Ready<Result<HttpResponse, Self::Error>>>;
|
||||
|
||||
fn future_respond_to(self, req: &HttpRequest) -> Self::Future {
|
||||
match self.0 {
|
||||
Some(t) => Either::Left(t.future_respond_to(req)),
|
||||
None => Either::Right(ready(Err(error::ErrorInternalServerError("err")))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Responder for StringResponder {
|
||||
type Body = BoxBody;
|
||||
|
||||
@@ -60,17 +39,6 @@ impl Responder for StringResponder {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Responder> Responder for OptionResponder<T> {
|
||||
type Body = BoxBody;
|
||||
|
||||
fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> {
|
||||
match self.0 {
|
||||
Some(t) => t.respond_to(req).map_into_boxed_body(),
|
||||
None => HttpResponse::from_error(error::ErrorInternalServerError("err")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn future_responder(c: &mut Criterion) {
|
||||
let rt = actix_rt::System::new();
|
||||
let req = TestRequest::default().to_http_request();
|
||||
|
127
actix-web/examples/middleware_from_fn.rs
Normal file
127
actix-web/examples/middleware_from_fn.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
//! Shows a couple of ways to use the `from_fn` middleware.
|
||||
|
||||
use std::{collections::HashMap, io, rc::Rc, time::Duration};
|
||||
|
||||
use actix_web::{
|
||||
body::MessageBody,
|
||||
dev::{Service, ServiceRequest, ServiceResponse, Transform},
|
||||
http::header::{self, HeaderValue, Range},
|
||||
middleware::{from_fn, Logger, Next},
|
||||
web::{self, Header, Query},
|
||||
App, Error, HttpResponse, HttpServer,
|
||||
};
|
||||
|
||||
async fn noop<B>(req: ServiceRequest, next: Next<B>) -> Result<ServiceResponse<B>, Error> {
|
||||
next.call(req).await
|
||||
}
|
||||
|
||||
async fn print_range_header<B>(
|
||||
range_header: Option<Header<Range>>,
|
||||
req: ServiceRequest,
|
||||
next: Next<B>,
|
||||
) -> Result<ServiceResponse<B>, Error> {
|
||||
if let Some(Header(range)) = range_header {
|
||||
println!("Range: {range}");
|
||||
} else {
|
||||
println!("No Range header");
|
||||
}
|
||||
|
||||
next.call(req).await
|
||||
}
|
||||
|
||||
async fn mutate_body_type(
|
||||
req: ServiceRequest,
|
||||
next: Next<impl MessageBody + 'static>,
|
||||
) -> Result<ServiceResponse<impl MessageBody>, Error> {
|
||||
let res = next.call(req).await?;
|
||||
Ok(res.map_into_left_body::<()>())
|
||||
}
|
||||
|
||||
async fn mutate_body_type_with_extractors(
|
||||
string_body: String,
|
||||
query: Query<HashMap<String, String>>,
|
||||
req: ServiceRequest,
|
||||
next: Next<impl MessageBody + 'static>,
|
||||
) -> Result<ServiceResponse<impl MessageBody>, Error> {
|
||||
println!("body is: {string_body}");
|
||||
println!("query string: {query:?}");
|
||||
|
||||
let res = next.call(req).await?;
|
||||
|
||||
Ok(res.map_body(move |_, _| string_body))
|
||||
}
|
||||
|
||||
async fn timeout_10secs(
|
||||
req: ServiceRequest,
|
||||
next: Next<impl MessageBody + 'static>,
|
||||
) -> Result<ServiceResponse<impl MessageBody>, Error> {
|
||||
match tokio::time::timeout(Duration::from_secs(10), next.call(req)).await {
|
||||
Ok(res) => res,
|
||||
Err(_err) => Err(actix_web::error::ErrorRequestTimeout("")),
|
||||
}
|
||||
}
|
||||
|
||||
struct MyMw(bool);
|
||||
|
||||
impl MyMw {
|
||||
async fn mw_cb(
|
||||
&self,
|
||||
req: ServiceRequest,
|
||||
next: Next<impl MessageBody + 'static>,
|
||||
) -> Result<ServiceResponse<impl MessageBody>, Error> {
|
||||
let mut res = match self.0 {
|
||||
true => req.into_response("short-circuited").map_into_right_body(),
|
||||
false => next.call(req).await?.map_into_left_body(),
|
||||
};
|
||||
|
||||
res.headers_mut()
|
||||
.insert(header::WARNING, HeaderValue::from_static("42"));
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn into_middleware<S, B>(
|
||||
self,
|
||||
) -> impl Transform<
|
||||
S,
|
||||
ServiceRequest,
|
||||
Response = ServiceResponse<impl MessageBody>,
|
||||
Error = Error,
|
||||
InitError = (),
|
||||
>
|
||||
where
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
let this = Rc::new(self);
|
||||
from_fn(move |req, next| {
|
||||
let this = Rc::clone(&this);
|
||||
async move { Self::mw_cb(&this, req, next).await }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||
|
||||
let bind = ("127.0.0.1", 8080);
|
||||
log::info!("staring server at http://{}:{}", &bind.0, &bind.1);
|
||||
|
||||
HttpServer::new(|| {
|
||||
App::new()
|
||||
.wrap(from_fn(noop))
|
||||
.wrap(from_fn(print_range_header))
|
||||
.wrap(from_fn(mutate_body_type))
|
||||
.wrap(from_fn(mutate_body_type_with_extractors))
|
||||
.wrap(from_fn(timeout_10secs))
|
||||
// switch bool to true to observe early response
|
||||
.wrap(MyMw(false).into_middleware())
|
||||
.wrap(Logger::default())
|
||||
.default_service(web::to(HttpResponse::Ok))
|
||||
})
|
||||
.workers(1)
|
||||
.bind(bind)?
|
||||
.run()
|
||||
.await
|
||||
}
|
@@ -39,7 +39,7 @@ impl App<AppEntry> {
|
||||
let factory_ref = Rc::new(RefCell::new(None));
|
||||
|
||||
App {
|
||||
endpoint: AppEntry::new(factory_ref.clone()),
|
||||
endpoint: AppEntry::new(Rc::clone(&factory_ref)),
|
||||
data_factories: Vec::new(),
|
||||
services: Vec::new(),
|
||||
default: None,
|
||||
|
@@ -71,7 +71,7 @@ where
|
||||
});
|
||||
|
||||
// create App config to pass to child services
|
||||
let mut config = AppService::new(config, default.clone());
|
||||
let mut config = AppService::new(config, Rc::clone(&default));
|
||||
|
||||
// register services
|
||||
mem::take(&mut *self.services.borrow_mut())
|
||||
|
@@ -68,7 +68,7 @@ impl AppService {
|
||||
pub(crate) fn clone_config(&self) -> Self {
|
||||
AppService {
|
||||
config: self.config.clone(),
|
||||
default: self.default.clone(),
|
||||
default: Rc::clone(&self.default),
|
||||
services: Vec::new(),
|
||||
root: false,
|
||||
}
|
||||
@@ -81,7 +81,7 @@ impl AppService {
|
||||
|
||||
/// Returns default handler factory.
|
||||
pub fn default_service(&self) -> Rc<BoxedHttpServiceFactory> {
|
||||
self.default.clone()
|
||||
Rc::clone(&self.default)
|
||||
}
|
||||
|
||||
/// Register HTTP service.
|
||||
|
@@ -184,7 +184,7 @@ impl<T: ?Sized + 'static> FromRequest for Data<T> {
|
||||
|
||||
impl<T: ?Sized + 'static> DataFactory for Data<T> {
|
||||
fn create(&self, extensions: &mut Extensions) -> bool {
|
||||
extensions.insert(Data(self.0.clone()));
|
||||
extensions.insert(Data(Arc::clone(&self.0)));
|
||||
true
|
||||
}
|
||||
}
|
||||
|
@@ -19,7 +19,7 @@ use crate::{
|
||||
/// 1. It is an async function (or a function/closure that returns an appropriate future);
|
||||
/// 1. The function parameters (up to 12) implement [`FromRequest`];
|
||||
/// 1. The async function (or future) resolves to a type that can be converted into an
|
||||
/// [`HttpResponse`] (i.e., it implements the [`Responder`] trait).
|
||||
/// [`HttpResponse`] (i.e., it implements the [`Responder`] trait).
|
||||
///
|
||||
///
|
||||
/// # Compiler Errors
|
||||
|
@@ -493,7 +493,7 @@ impl Header for ContentDisposition {
|
||||
}
|
||||
|
||||
fn parse<T: crate::HttpMessage>(msg: &T) -> Result<Self, crate::error::ParseError> {
|
||||
if let Some(h) = msg.headers().get(&Self::name()) {
|
||||
if let Some(h) = msg.headers().get(Self::name()) {
|
||||
Self::from_raw(h)
|
||||
} else {
|
||||
Err(crate::error::ParseError::Header)
|
||||
|
@@ -107,16 +107,16 @@ impl ByteRangeSpec {
|
||||
/// satisfiable if they meet the following conditions:
|
||||
///
|
||||
/// > If a valid byte-range-set includes at least one byte-range-spec with a first-byte-pos that
|
||||
/// is less than the current length of the representation, or at least one
|
||||
/// suffix-byte-range-spec with a non-zero suffix-length, then the byte-range-set
|
||||
/// is satisfiable. Otherwise, the byte-range-set is unsatisfiable.
|
||||
/// > is less than the current length of the representation, or at least one
|
||||
/// > suffix-byte-range-spec with a non-zero suffix-length, then the byte-range-set is
|
||||
/// > satisfiable. Otherwise, the byte-range-set is unsatisfiable.
|
||||
///
|
||||
/// The function also computes remainder ranges based on the RFC:
|
||||
///
|
||||
/// > If the last-byte-pos value is absent, or if the value is greater than or equal to the
|
||||
/// current length of the representation data, the byte range is interpreted as the remainder
|
||||
/// of the representation (i.e., the server replaces the value of last-byte-pos with a value
|
||||
/// that is one less than the current length of the selected representation).
|
||||
/// > current length of the representation data, the byte range is interpreted as the remainder
|
||||
/// > of the representation (i.e., the server replaces the value of last-byte-pos with a value
|
||||
/// > that is one less than the current length of the selected representation).
|
||||
///
|
||||
/// [RFC 7233 §2.1]: https://datatracker.ietf.org/doc/html/rfc7233
|
||||
pub fn to_satisfiable_range(&self, full_length: u64) -> Option<(u64, u64)> {
|
||||
@@ -270,7 +270,7 @@ impl Header for Range {
|
||||
|
||||
#[inline]
|
||||
fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError> {
|
||||
header::from_one_raw_str(msg.headers().get(&Self::name()))
|
||||
header::from_one_raw_str(msg.headers().get(Self::name()))
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -70,8 +70,6 @@
|
||||
//! - `rustls-0_23` - HTTPS support via `rustls` 0.23 crate, supports `HTTP/2`
|
||||
//! - `secure-cookies` - secure cookies support
|
||||
|
||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||
#![warn(future_incompatible)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
@@ -106,6 +104,7 @@ mod scope;
|
||||
mod server;
|
||||
mod service;
|
||||
pub mod test;
|
||||
mod thin_data;
|
||||
pub(crate) mod types;
|
||||
pub mod web;
|
||||
|
||||
|
@@ -141,7 +141,7 @@ where
|
||||
actix_service::forward_ready!(service);
|
||||
|
||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||
let inner = self.inner.clone();
|
||||
let inner = Rc::clone(&self.inner);
|
||||
let fut = self.service.call(req);
|
||||
|
||||
DefaultHeaderFuture {
|
||||
|
@@ -220,16 +220,20 @@ impl<B> ErrorHandlers<B> {
|
||||
/// [`.handler()`][ErrorHandlers::handler]) will fall back on this.
|
||||
///
|
||||
/// Note that this will overwrite any default handlers previously set by calling
|
||||
/// [`.default_handler_client()`][ErrorHandlers::default_handler_client] or
|
||||
/// [`.default_handler_server()`][ErrorHandlers::default_handler_server], but not any set by
|
||||
/// calling [`.handler()`][ErrorHandlers::handler].
|
||||
/// [`default_handler_client()`] or [`.default_handler_server()`], but not any set by calling
|
||||
/// [`.handler()`].
|
||||
///
|
||||
/// [`default_handler_client()`]: ErrorHandlers::default_handler_client
|
||||
/// [`.default_handler_server()`]: ErrorHandlers::default_handler_server
|
||||
/// [`.handler()`]: ErrorHandlers::handler
|
||||
pub fn default_handler<F>(self, handler: F) -> Self
|
||||
where
|
||||
F: Fn(ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> + 'static,
|
||||
{
|
||||
let handler = Rc::new(handler);
|
||||
let handler2 = Rc::clone(&handler);
|
||||
Self {
|
||||
default_server: Some(handler.clone()),
|
||||
default_server: Some(handler2),
|
||||
default_client: Some(handler),
|
||||
..self
|
||||
}
|
||||
@@ -288,7 +292,7 @@ where
|
||||
type Future = LocalBoxFuture<'static, Result<Self::Transform, Self::InitError>>;
|
||||
|
||||
fn new_transform(&self, service: S) -> Self::Future {
|
||||
let handlers = self.handlers.clone();
|
||||
let handlers = Rc::clone(&self.handlers);
|
||||
let default_client = self.default_client.clone();
|
||||
let default_server = self.default_server.clone();
|
||||
Box::pin(async move {
|
||||
@@ -323,7 +327,7 @@ where
|
||||
actix_service::forward_ready!(service);
|
||||
|
||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||
let handlers = self.handlers.clone();
|
||||
let handlers = Rc::clone(&self.handlers);
|
||||
let default_client = self.default_client.clone();
|
||||
let default_server = self.default_server.clone();
|
||||
let fut = self.service.call(req);
|
||||
|
349
actix-web/src/middleware/from_fn.rs
Normal file
349
actix-web/src/middleware/from_fn.rs
Normal file
@@ -0,0 +1,349 @@
|
||||
use std::{future::Future, marker::PhantomData, rc::Rc};
|
||||
|
||||
use actix_service::boxed::{self, BoxFuture, RcService};
|
||||
use actix_utils::future::{ready, Ready};
|
||||
use futures_core::future::LocalBoxFuture;
|
||||
|
||||
use crate::{
|
||||
body::MessageBody,
|
||||
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
|
||||
Error, FromRequest,
|
||||
};
|
||||
|
||||
/// Wraps an async function to be used as a middleware.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// The wrapped function should have the following form:
|
||||
///
|
||||
/// ```
|
||||
/// # use actix_web::{
|
||||
/// # App, Error,
|
||||
/// # body::MessageBody,
|
||||
/// # dev::{ServiceRequest, ServiceResponse, Service as _},
|
||||
/// # };
|
||||
/// use actix_web::middleware::{self, Next};
|
||||
///
|
||||
/// async fn my_mw(
|
||||
/// req: ServiceRequest,
|
||||
/// next: Next<impl MessageBody>,
|
||||
/// ) -> Result<ServiceResponse<impl MessageBody>, Error> {
|
||||
/// // pre-processing
|
||||
/// next.call(req).await
|
||||
/// // post-processing
|
||||
/// }
|
||||
/// # App::new().wrap(middleware::from_fn(my_mw));
|
||||
/// ```
|
||||
///
|
||||
/// Then use in an app builder like this:
|
||||
///
|
||||
/// ```
|
||||
/// use actix_web::{
|
||||
/// App, Error,
|
||||
/// dev::{ServiceRequest, ServiceResponse, Service as _},
|
||||
/// };
|
||||
/// use actix_web::middleware::from_fn;
|
||||
/// # use actix_web::middleware::Next;
|
||||
/// # async fn my_mw<B>(req: ServiceRequest, next: Next<B>) -> Result<ServiceResponse<B>, Error> {
|
||||
/// # next.call(req).await
|
||||
/// # }
|
||||
///
|
||||
/// App::new()
|
||||
/// .wrap(from_fn(my_mw))
|
||||
/// # ;
|
||||
/// ```
|
||||
///
|
||||
/// It is also possible to write a middleware that automatically uses extractors, similar to request
|
||||
/// handlers, by declaring them as the first parameters. As usual, **take care with extractors that
|
||||
/// consume the body stream**, since handlers will no longer be able to read it again without
|
||||
/// putting the body "back" into the request object within your middleware.
|
||||
///
|
||||
/// ```
|
||||
/// # use std::collections::HashMap;
|
||||
/// # use actix_web::{
|
||||
/// # App, Error,
|
||||
/// # body::MessageBody,
|
||||
/// # dev::{ServiceRequest, ServiceResponse},
|
||||
/// # http::header::{Accept, Date},
|
||||
/// # web::{Header, Query},
|
||||
/// # };
|
||||
/// use actix_web::middleware::Next;
|
||||
///
|
||||
/// async fn my_extracting_mw(
|
||||
/// accept: Header<Accept>,
|
||||
/// query: Query<HashMap<String, String>>,
|
||||
/// req: ServiceRequest,
|
||||
/// next: Next<impl MessageBody>,
|
||||
/// ) -> Result<ServiceResponse<impl MessageBody>, Error> {
|
||||
/// // pre-processing
|
||||
/// next.call(req).await
|
||||
/// // post-processing
|
||||
/// }
|
||||
/// # App::new().wrap(actix_web::middleware::from_fn(my_extracting_mw));
|
||||
pub fn from_fn<F, Es>(mw_fn: F) -> MiddlewareFn<F, Es> {
|
||||
MiddlewareFn {
|
||||
mw_fn: Rc::new(mw_fn),
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Middleware transform for [`from_fn`].
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct MiddlewareFn<F, Es> {
|
||||
mw_fn: Rc<F>,
|
||||
_phantom: PhantomData<Es>,
|
||||
}
|
||||
|
||||
impl<S, F, Fut, B, B2> Transform<S, ServiceRequest> for MiddlewareFn<F, ()>
|
||||
where
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
||||
F: Fn(ServiceRequest, Next<B>) -> Fut + 'static,
|
||||
Fut: Future<Output = Result<ServiceResponse<B2>, Error>>,
|
||||
B2: MessageBody,
|
||||
{
|
||||
type Response = ServiceResponse<B2>;
|
||||
type Error = Error;
|
||||
type Transform = MiddlewareFnService<F, B, ()>;
|
||||
type InitError = ();
|
||||
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||
|
||||
fn new_transform(&self, service: S) -> Self::Future {
|
||||
ready(Ok(MiddlewareFnService {
|
||||
service: boxed::rc_service(service),
|
||||
mw_fn: Rc::clone(&self.mw_fn),
|
||||
_phantom: PhantomData,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Middleware service for [`from_fn`].
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct MiddlewareFnService<F, B, Es> {
|
||||
service: RcService<ServiceRequest, ServiceResponse<B>, Error>,
|
||||
mw_fn: Rc<F>,
|
||||
_phantom: PhantomData<(B, Es)>,
|
||||
}
|
||||
|
||||
impl<F, Fut, B, B2> Service<ServiceRequest> for MiddlewareFnService<F, B, ()>
|
||||
where
|
||||
F: Fn(ServiceRequest, Next<B>) -> Fut,
|
||||
Fut: Future<Output = Result<ServiceResponse<B2>, Error>>,
|
||||
B2: MessageBody,
|
||||
{
|
||||
type Response = ServiceResponse<B2>;
|
||||
type Error = Error;
|
||||
type Future = Fut;
|
||||
|
||||
forward_ready!(service);
|
||||
|
||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||
(self.mw_fn)(
|
||||
req,
|
||||
Next::<B> {
|
||||
service: Rc::clone(&self.service),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_middleware_fn_service {
|
||||
($($ext_type:ident),*) => {
|
||||
impl<S, F, Fut, B, B2, $($ext_type),*> Transform<S, ServiceRequest> for MiddlewareFn<F, ($($ext_type),*,)>
|
||||
where
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
||||
F: Fn($($ext_type),*, ServiceRequest, Next<B>) -> Fut + 'static,
|
||||
$($ext_type: FromRequest + 'static,)*
|
||||
Fut: Future<Output = Result<ServiceResponse<B2>, Error>> + 'static,
|
||||
B: MessageBody + 'static,
|
||||
B2: MessageBody + 'static,
|
||||
{
|
||||
type Response = ServiceResponse<B2>;
|
||||
type Error = Error;
|
||||
type Transform = MiddlewareFnService<F, B, ($($ext_type,)*)>;
|
||||
type InitError = ();
|
||||
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||
|
||||
fn new_transform(&self, service: S) -> Self::Future {
|
||||
ready(Ok(MiddlewareFnService {
|
||||
service: boxed::rc_service(service),
|
||||
mw_fn: Rc::clone(&self.mw_fn),
|
||||
_phantom: PhantomData,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, $($ext_type),*, Fut, B: 'static, B2> Service<ServiceRequest>
|
||||
for MiddlewareFnService<F, B, ($($ext_type),*,)>
|
||||
where
|
||||
F: Fn(
|
||||
$($ext_type),*,
|
||||
ServiceRequest,
|
||||
Next<B>
|
||||
) -> Fut + 'static,
|
||||
$($ext_type: FromRequest + 'static,)*
|
||||
Fut: Future<Output = Result<ServiceResponse<B2>, Error>> + 'static,
|
||||
B2: MessageBody + 'static,
|
||||
{
|
||||
type Response = ServiceResponse<B2>;
|
||||
type Error = Error;
|
||||
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||
|
||||
forward_ready!(service);
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
fn call(&self, mut req: ServiceRequest) -> Self::Future {
|
||||
let mw_fn = Rc::clone(&self.mw_fn);
|
||||
let service = Rc::clone(&self.service);
|
||||
|
||||
Box::pin(async move {
|
||||
let ($($ext_type,)*) = req.extract::<($($ext_type,)*)>().await?;
|
||||
|
||||
(mw_fn)($($ext_type),*, req, Next::<B> { service }).await
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_middleware_fn_service!(E1);
|
||||
impl_middleware_fn_service!(E1, E2);
|
||||
impl_middleware_fn_service!(E1, E2, E3);
|
||||
impl_middleware_fn_service!(E1, E2, E3, E4);
|
||||
impl_middleware_fn_service!(E1, E2, E3, E4, E5);
|
||||
impl_middleware_fn_service!(E1, E2, E3, E4, E5, E6);
|
||||
impl_middleware_fn_service!(E1, E2, E3, E4, E5, E6, E7);
|
||||
impl_middleware_fn_service!(E1, E2, E3, E4, E5, E6, E7, E8);
|
||||
impl_middleware_fn_service!(E1, E2, E3, E4, E5, E6, E7, E8, E9);
|
||||
|
||||
/// Wraps the "next" service in the middleware chain.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Next<B> {
|
||||
service: RcService<ServiceRequest, ServiceResponse<B>, Error>,
|
||||
}
|
||||
|
||||
impl<B> Next<B> {
|
||||
/// Equivalent to `Service::call(self, req)`.
|
||||
pub fn call(&self, req: ServiceRequest) -> <Self as Service<ServiceRequest>>::Future {
|
||||
Service::call(self, req)
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> Service<ServiceRequest> for Next<B> {
|
||||
type Response = ServiceResponse<B>;
|
||||
type Error = Error;
|
||||
type Future = BoxFuture<Result<Self::Response, Self::Error>>;
|
||||
|
||||
forward_ready!(service);
|
||||
|
||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||
self.service.call(req)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
http::header::{self, HeaderValue},
|
||||
middleware::{Compat, Logger},
|
||||
test, web, App, HttpResponse,
|
||||
};
|
||||
|
||||
async fn noop<B>(req: ServiceRequest, next: Next<B>) -> Result<ServiceResponse<B>, Error> {
|
||||
next.call(req).await
|
||||
}
|
||||
|
||||
async fn add_res_header<B>(
|
||||
req: ServiceRequest,
|
||||
next: Next<B>,
|
||||
) -> Result<ServiceResponse<B>, Error> {
|
||||
let mut res = next.call(req).await?;
|
||||
res.headers_mut()
|
||||
.insert(header::WARNING, HeaderValue::from_static("42"));
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
async fn mutate_body_type(
|
||||
req: ServiceRequest,
|
||||
next: Next<impl MessageBody + 'static>,
|
||||
) -> Result<ServiceResponse<impl MessageBody>, Error> {
|
||||
let res = next.call(req).await?;
|
||||
Ok(res.map_into_left_body::<()>())
|
||||
}
|
||||
|
||||
struct MyMw(bool);
|
||||
|
||||
impl MyMw {
|
||||
async fn mw_cb(
|
||||
&self,
|
||||
req: ServiceRequest,
|
||||
next: Next<impl MessageBody + 'static>,
|
||||
) -> Result<ServiceResponse<impl MessageBody>, Error> {
|
||||
let mut res = match self.0 {
|
||||
true => req.into_response("short-circuited").map_into_right_body(),
|
||||
false => next.call(req).await?.map_into_left_body(),
|
||||
};
|
||||
res.headers_mut()
|
||||
.insert(header::WARNING, HeaderValue::from_static("42"));
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn into_middleware<S, B>(
|
||||
self,
|
||||
) -> impl Transform<
|
||||
S,
|
||||
ServiceRequest,
|
||||
Response = ServiceResponse<impl MessageBody>,
|
||||
Error = Error,
|
||||
InitError = (),
|
||||
>
|
||||
where
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
let this = Rc::new(self);
|
||||
from_fn(move |req, next| {
|
||||
let this = Rc::clone(&this);
|
||||
async move { Self::mw_cb(&this, req, next).await }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn compat_compat() {
|
||||
let _ = App::new().wrap(Compat::new(from_fn(noop)));
|
||||
let _ = App::new().wrap(Compat::new(from_fn(mutate_body_type)));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn permits_different_in_and_out_body_types() {
|
||||
let app = test::init_service(
|
||||
App::new()
|
||||
.wrap(from_fn(mutate_body_type))
|
||||
.wrap(from_fn(add_res_header))
|
||||
.wrap(Logger::default())
|
||||
.wrap(from_fn(noop))
|
||||
.default_service(web::to(HttpResponse::NotFound)),
|
||||
)
|
||||
.await;
|
||||
|
||||
let req = test::TestRequest::default().to_request();
|
||||
let res = test::call_service(&app, req).await;
|
||||
assert!(res.headers().contains_key(header::WARNING));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn closure_capture_and_return_from_fn() {
|
||||
let app = test::init_service(
|
||||
App::new()
|
||||
.wrap(Logger::default())
|
||||
.wrap(MyMw(true).into_middleware())
|
||||
.wrap(Logger::default()),
|
||||
)
|
||||
.await;
|
||||
|
||||
let req = test::TestRequest::default().to_request();
|
||||
let res = test::call_service(&app, req).await;
|
||||
assert!(res.headers().contains_key(header::WARNING));
|
||||
}
|
||||
}
|
@@ -276,7 +276,7 @@ where
|
||||
|
||||
ready(Ok(LoggerMiddleware {
|
||||
service,
|
||||
inner: self.0.clone(),
|
||||
inner: Rc::clone(&self.0),
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -622,11 +622,7 @@ impl FormatText {
|
||||
|
||||
FormatText::ResponseHeader(ref name) => {
|
||||
let s = if let Some(val) = res.headers().get(name) {
|
||||
if let Ok(s) = val.to_str() {
|
||||
s
|
||||
} else {
|
||||
"-"
|
||||
}
|
||||
val.to_str().unwrap_or("-")
|
||||
} else {
|
||||
"-"
|
||||
};
|
||||
@@ -670,11 +666,7 @@ impl FormatText {
|
||||
FormatText::RequestTime => *self = FormatText::Str(now.format(&Rfc3339).unwrap()),
|
||||
FormatText::RequestHeader(ref name) => {
|
||||
let s = if let Some(val) = req.headers().get(name) {
|
||||
if let Ok(s) = val.to_str() {
|
||||
s
|
||||
} else {
|
||||
"-"
|
||||
}
|
||||
val.to_str().unwrap_or("-")
|
||||
} else {
|
||||
"-"
|
||||
};
|
||||
|
@@ -15,10 +15,47 @@
|
||||
//! - Access external services (e.g., [sessions](https://docs.rs/actix-session), etc.)
|
||||
//!
|
||||
//! Middleware is registered for each [`App`], [`Scope`](crate::Scope), or
|
||||
//! [`Resource`](crate::Resource) and executed in opposite order as registration. In general, a
|
||||
//! middleware is a pair of types that implements the [`Service`] trait and [`Transform`] trait,
|
||||
//! respectively. The [`new_transform`] and [`call`] methods must return a [`Future`], though it
|
||||
//! can often be [an immediately-ready one](actix_utils::future::Ready).
|
||||
//! [`Resource`](crate::Resource) and executed in opposite order as registration.
|
||||
//!
|
||||
//! # Simple Middleware
|
||||
//!
|
||||
//! In many cases, you can model your middleware as an async function via the [`from_fn()`] helper
|
||||
//! that provides a natural interface for implementing your desired behaviors.
|
||||
//!
|
||||
//! ```
|
||||
//! # use actix_web::{
|
||||
//! # App, Error,
|
||||
//! # body::MessageBody,
|
||||
//! # dev::{ServiceRequest, ServiceResponse, Service as _},
|
||||
//! # };
|
||||
//! use actix_web::middleware::{self, Next};
|
||||
//!
|
||||
//! async fn my_mw(
|
||||
//! req: ServiceRequest,
|
||||
//! next: Next<impl MessageBody>,
|
||||
//! ) -> Result<ServiceResponse<impl MessageBody>, Error> {
|
||||
//! // pre-processing
|
||||
//!
|
||||
//! // invoke the wrapped middleware or service
|
||||
//! let res = next.call(req).await?;
|
||||
//!
|
||||
//! // post-processing
|
||||
//!
|
||||
//! Ok(res)
|
||||
//! }
|
||||
//!
|
||||
//! App::new()
|
||||
//! .wrap(middleware::from_fn(my_mw));
|
||||
//! ```
|
||||
//!
|
||||
//! ## Complex Middleware
|
||||
//!
|
||||
//! In the more general ase, a middleware is a pair of types that implements the [`Service`] trait
|
||||
//! and [`Transform`] trait, respectively. The [`new_transform`] and [`call`] methods must return a
|
||||
//! [`Future`], though it can often be [an immediately-ready one](actix_utils::future::Ready).
|
||||
//!
|
||||
//! All the built-in middleware use this pattern with pairs of builder (`Transform`) +
|
||||
//! implementation (`Service`) types.
|
||||
//!
|
||||
//! # Ordering
|
||||
//!
|
||||
@@ -67,7 +104,7 @@
|
||||
//! Response
|
||||
//! ```
|
||||
//! The request _first_ gets processed by the middleware specified _last_ - `MiddlewareC`. It passes
|
||||
//! the request (modified a modified one) to the next middleware - `MiddlewareB` - _or_ directly
|
||||
//! the request (possibly a modified one) to the next middleware - `MiddlewareB` - _or_ directly
|
||||
//! responds to the request (e.g. when the request was invalid or an error occurred). `MiddlewareB`
|
||||
//! processes the request as well and passes it to `MiddlewareA`, which then passes it to the
|
||||
//! [`Service`]. In the [`Service`], the extractors will run first. They don't pass the request on,
|
||||
@@ -196,18 +233,6 @@
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! # Simpler Middleware
|
||||
//!
|
||||
//! In many cases, you _can_ actually use an async function via a helper that will provide a more
|
||||
//! natural flow for your behavior.
|
||||
//!
|
||||
//! The experimental `actix_web_lab` crate provides a [`from_fn`][lab_from_fn] utility which allows
|
||||
//! an async fn to be wrapped and used in the same way as other middleware. See the
|
||||
//! [`from_fn`][lab_from_fn] docs for more info and examples of it's use.
|
||||
//!
|
||||
//! While [`from_fn`][lab_from_fn] is experimental currently, it's likely this helper will graduate
|
||||
//! to Actix Web in some form, so feedback is appreciated.
|
||||
//!
|
||||
//! [`Future`]: std::future::Future
|
||||
//! [`App`]: crate::App
|
||||
//! [`FromRequest`]: crate::FromRequest
|
||||
@@ -215,7 +240,7 @@
|
||||
//! [`Transform`]: crate::dev::Transform
|
||||
//! [`call`]: crate::dev::Service::call()
|
||||
//! [`new_transform`]: crate::dev::Transform::new_transform()
|
||||
//! [lab_from_fn]: https://docs.rs/actix-web-lab/latest/actix_web_lab/middleware/fn.from_fn.html
|
||||
//! [`from_fn`]: crate
|
||||
|
||||
mod compat;
|
||||
#[cfg(feature = "__compress")]
|
||||
@@ -223,6 +248,7 @@ mod compress;
|
||||
mod condition;
|
||||
mod default_headers;
|
||||
mod err_handlers;
|
||||
mod from_fn;
|
||||
mod identity;
|
||||
mod logger;
|
||||
mod normalize;
|
||||
@@ -234,6 +260,7 @@ pub use self::{
|
||||
condition::Condition,
|
||||
default_headers::DefaultHeaders,
|
||||
err_handlers::{ErrorHandlerResponse, ErrorHandlers},
|
||||
from_fn::{from_fn, Next},
|
||||
identity::Identity,
|
||||
logger::Logger,
|
||||
normalize::{NormalizePath, TrailingSlash},
|
||||
|
@@ -62,14 +62,14 @@ pub struct Resource<T = ResourceEndpoint> {
|
||||
impl Resource {
|
||||
/// Constructs new resource that matches a `path` pattern.
|
||||
pub fn new<T: IntoPatterns>(path: T) -> Resource {
|
||||
let fref = Rc::new(RefCell::new(None));
|
||||
let factory_ref = Rc::new(RefCell::new(None));
|
||||
|
||||
Resource {
|
||||
routes: Vec::new(),
|
||||
rdef: path.patterns(),
|
||||
name: None,
|
||||
endpoint: ResourceEndpoint::new(fref.clone()),
|
||||
factory_ref: fref,
|
||||
endpoint: ResourceEndpoint::new(Rc::clone(&factory_ref)),
|
||||
factory_ref,
|
||||
guards: Vec::new(),
|
||||
app_data: None,
|
||||
default: boxed::factory(fn_service(|req: ServiceRequest| async {
|
||||
|
@@ -463,7 +463,7 @@ mod tests {
|
||||
// content type override
|
||||
let res = HttpResponse::Ok()
|
||||
.insert_header((CONTENT_TYPE, "text/json"))
|
||||
.json(&vec!["v1", "v2", "v3"]);
|
||||
.json(["v1", "v2", "v3"]);
|
||||
let ct = res.headers().get(CONTENT_TYPE).unwrap();
|
||||
assert_eq!(ct, HeaderValue::from_static("text/json"));
|
||||
assert_body_eq!(res, br#"["v1","v2","v3"]"#);
|
||||
|
@@ -77,7 +77,7 @@ impl ServiceFactory<ServiceRequest> for Route {
|
||||
|
||||
fn new_service(&self, _: ()) -> Self::Future {
|
||||
let fut = self.service.new_service(());
|
||||
let guards = self.guards.clone();
|
||||
let guards = Rc::clone(&self.guards);
|
||||
|
||||
Box::pin(async move {
|
||||
let service = fut.await?;
|
||||
|
@@ -510,7 +510,7 @@ where
|
||||
/// No changes are made to `lst`'s configuration. Ensure it is configured properly before
|
||||
/// passing ownership to `listen()`.
|
||||
pub fn listen(mut self, lst: net::TcpListener) -> io::Result<Self> {
|
||||
let cfg = self.config.clone();
|
||||
let cfg = Arc::clone(&self.config);
|
||||
let factory = self.factory.clone();
|
||||
let addr = lst.local_addr().unwrap();
|
||||
|
||||
@@ -554,7 +554,7 @@ where
|
||||
/// Binds to existing listener for accepting incoming plaintext HTTP/1.x or HTTP/2 connections.
|
||||
#[cfg(feature = "http2")]
|
||||
pub fn listen_auto_h2c(mut self, lst: net::TcpListener) -> io::Result<Self> {
|
||||
let cfg = self.config.clone();
|
||||
let cfg = Arc::clone(&self.config);
|
||||
let factory = self.factory.clone();
|
||||
let addr = lst.local_addr().unwrap();
|
||||
|
||||
@@ -632,7 +632,7 @@ where
|
||||
config: actix_tls::accept::rustls_0_20::reexports::ServerConfig,
|
||||
) -> io::Result<Self> {
|
||||
let factory = self.factory.clone();
|
||||
let cfg = self.config.clone();
|
||||
let cfg = Arc::clone(&self.config);
|
||||
let addr = lst.local_addr().unwrap();
|
||||
self.sockets.push(Socket {
|
||||
addr,
|
||||
@@ -683,7 +683,7 @@ where
|
||||
config: actix_tls::accept::rustls_0_21::reexports::ServerConfig,
|
||||
) -> io::Result<Self> {
|
||||
let factory = self.factory.clone();
|
||||
let cfg = self.config.clone();
|
||||
let cfg = Arc::clone(&self.config);
|
||||
let addr = lst.local_addr().unwrap();
|
||||
self.sockets.push(Socket {
|
||||
addr,
|
||||
@@ -749,7 +749,7 @@ where
|
||||
config: actix_tls::accept::rustls_0_22::reexports::ServerConfig,
|
||||
) -> io::Result<Self> {
|
||||
let factory = self.factory.clone();
|
||||
let cfg = self.config.clone();
|
||||
let cfg = Arc::clone(&self.config);
|
||||
let addr = lst.local_addr().unwrap();
|
||||
self.sockets.push(Socket {
|
||||
addr,
|
||||
@@ -815,7 +815,7 @@ where
|
||||
config: actix_tls::accept::rustls_0_23::reexports::ServerConfig,
|
||||
) -> io::Result<Self> {
|
||||
let factory = self.factory.clone();
|
||||
let cfg = self.config.clone();
|
||||
let cfg = Arc::clone(&self.config);
|
||||
let addr = lst.local_addr().unwrap();
|
||||
self.sockets.push(Socket {
|
||||
addr,
|
||||
@@ -880,7 +880,7 @@ where
|
||||
acceptor: SslAcceptor,
|
||||
) -> io::Result<Self> {
|
||||
let factory = self.factory.clone();
|
||||
let cfg = self.config.clone();
|
||||
let cfg = Arc::clone(&self.config);
|
||||
let addr = lst.local_addr().unwrap();
|
||||
self.sockets.push(Socket {
|
||||
addr,
|
||||
@@ -937,7 +937,7 @@ where
|
||||
use actix_rt::net::UnixStream;
|
||||
use actix_service::{fn_service, ServiceFactoryExt as _};
|
||||
|
||||
let cfg = self.config.clone();
|
||||
let cfg = Arc::clone(&self.config);
|
||||
let factory = self.factory.clone();
|
||||
let socket_addr =
|
||||
net::SocketAddr::new(net::IpAddr::V4(net::Ipv4Addr::new(127, 0, 0, 1)), 8080);
|
||||
@@ -982,7 +982,7 @@ where
|
||||
use actix_rt::net::UnixStream;
|
||||
use actix_service::{fn_service, ServiceFactoryExt as _};
|
||||
|
||||
let cfg = self.config.clone();
|
||||
let cfg = Arc::clone(&self.config);
|
||||
let factory = self.factory.clone();
|
||||
let socket_addr =
|
||||
net::SocketAddr::new(net::IpAddr::V4(net::Ipv4Addr::new(127, 0, 0, 1)), 8080);
|
||||
|
121
actix-web/src/thin_data.rs
Normal file
121
actix-web/src/thin_data.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
use std::any::type_name;
|
||||
|
||||
use actix_utils::future::{ready, Ready};
|
||||
|
||||
use crate::{dev::Payload, error, FromRequest, HttpRequest};
|
||||
|
||||
/// Application data wrapper and extractor for cheaply-cloned types.
|
||||
///
|
||||
/// Similar to the [`Data`] wrapper but for `Clone`/`Copy` types that are already an `Arc` internally,
|
||||
/// share state using some other means when cloned, or is otherwise static data that is very cheap
|
||||
/// to clone.
|
||||
///
|
||||
/// Unlike `Data`, this wrapper clones `T` during extraction. Therefore, it is the user's
|
||||
/// responsibility to ensure that clones of `T` do actually share the same state, otherwise state
|
||||
/// may be unexpectedly different across multiple requests.
|
||||
///
|
||||
/// Note that if your type is literally an `Arc<T>` then it's recommended to use the
|
||||
/// [`Data::from(arc)`][data_from_arc] conversion instead.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use actix_web::{
|
||||
/// web::{self, ThinData},
|
||||
/// App, HttpResponse, Responder,
|
||||
/// };
|
||||
///
|
||||
/// // Use the `ThinData<T>` extractor to access a database connection pool.
|
||||
/// async fn index(ThinData(db_pool): ThinData<DbPool>) -> impl Responder {
|
||||
/// // database action ...
|
||||
///
|
||||
/// HttpResponse::Ok()
|
||||
/// }
|
||||
///
|
||||
/// # type DbPool = ();
|
||||
/// let db_pool = DbPool::default();
|
||||
///
|
||||
/// App::new()
|
||||
/// .app_data(ThinData(db_pool.clone()))
|
||||
/// .service(web::resource("/").get(index))
|
||||
/// # ;
|
||||
/// ```
|
||||
///
|
||||
/// [`Data`]: crate::web::Data
|
||||
/// [data_from_arc]: crate::web::Data#impl-From<Arc<T>>-for-Data<T>
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ThinData<T>(pub T);
|
||||
|
||||
impl_more::impl_as_ref!(ThinData<T> => T);
|
||||
impl_more::impl_as_mut!(ThinData<T> => T);
|
||||
impl_more::impl_deref_and_mut!(<T> in ThinData<T> => T);
|
||||
|
||||
impl<T: Clone + 'static> FromRequest for ThinData<T> {
|
||||
type Error = crate::Error;
|
||||
type Future = Ready<Result<Self, Self::Error>>;
|
||||
|
||||
#[inline]
|
||||
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||
ready(req.app_data::<Self>().cloned().ok_or_else(|| {
|
||||
log::debug!(
|
||||
"Failed to extract `ThinData<{}>` for `{}` handler. For the ThinData extractor to work \
|
||||
correctly, wrap the data with `ThinData()` and pass it to `App::app_data()`. \
|
||||
Ensure that types align in both the set and retrieve calls.",
|
||||
type_name::<T>(),
|
||||
req.match_name().unwrap_or(req.path())
|
||||
);
|
||||
|
||||
error::ErrorInternalServerError(
|
||||
"Requested application data is not configured correctly. \
|
||||
View/enable debug logs for more details.",
|
||||
)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
http::StatusCode,
|
||||
test::{call_service, init_service, TestRequest},
|
||||
web, App, HttpResponse,
|
||||
};
|
||||
|
||||
type TestT = Arc<Mutex<u32>>;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn thin_data() {
|
||||
let test_data = TestT::default();
|
||||
|
||||
let app = init_service(App::new().app_data(ThinData(test_data.clone())).service(
|
||||
web::resource("/").to(|td: ThinData<TestT>| {
|
||||
*td.lock().unwrap() += 1;
|
||||
HttpResponse::Ok()
|
||||
}),
|
||||
))
|
||||
.await;
|
||||
|
||||
for _ in 0..3 {
|
||||
let req = TestRequest::default().to_request();
|
||||
let resp = call_service(&app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
assert_eq!(*test_data.lock().unwrap(), 3);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn thin_data_missing() {
|
||||
let app = init_service(
|
||||
App::new().service(web::resource("/").to(|_: ThinData<u32>| HttpResponse::Ok())),
|
||||
)
|
||||
.await;
|
||||
|
||||
let req = TestRequest::default().to_request();
|
||||
let resp = call_service(&app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
@@ -2,6 +2,7 @@
|
||||
//!
|
||||
//! # Request Extractors
|
||||
//! - [`Data`]: Application data item
|
||||
//! - [`ThinData`]: Cheap-to-clone application data item
|
||||
//! - [`ReqData`]: Request-local data item
|
||||
//! - [`Path`]: URL path parameters / dynamic segments
|
||||
//! - [`Query`]: URL query parameters
|
||||
@@ -22,7 +23,8 @@ use actix_router::IntoPatterns;
|
||||
pub use bytes::{Buf, BufMut, Bytes, BytesMut};
|
||||
|
||||
pub use crate::{
|
||||
config::ServiceConfig, data::Data, redirect::Redirect, request_data::ReqData, types::*,
|
||||
config::ServiceConfig, data::Data, redirect::Redirect, request_data::ReqData,
|
||||
thin_data::ThinData, types::*,
|
||||
};
|
||||
use crate::{
|
||||
error::BlockingError, http::Method, service::WebService, FromRequest, Handler, Resource,
|
||||
|
@@ -153,6 +153,9 @@ tokio = { version = "1.24.2", features = ["rt-multi-thread", "macros"] }
|
||||
zstd = "0.13"
|
||||
tls-rustls-0_23 = { package = "rustls", version = "0.23" } # add rustls 0.23 with default features to make aws_lc_rs work in tests
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[[example]]
|
||||
name = "client"
|
||||
required-features = ["rustls-0_23-webpki-roots"]
|
||||
|
@@ -163,6 +163,7 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[allow(dead_code)]
|
||||
struct PinType(PhantomPinned);
|
||||
|
||||
impl MessageBody for PinType {
|
||||
|
@@ -173,12 +173,15 @@ where
|
||||
};
|
||||
|
||||
// acquire an owned permit and carry it with connection
|
||||
let permit = inner.permits.clone().acquire_owned().await.map_err(|_| {
|
||||
ConnectError::Io(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"failed to acquire semaphore on client connection pool",
|
||||
))
|
||||
})?;
|
||||
let permit = Arc::clone(&inner.permits)
|
||||
.acquire_owned()
|
||||
.await
|
||||
.map_err(|_| {
|
||||
ConnectError::Io(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"failed to acquire semaphore on client connection pool",
|
||||
))
|
||||
})?;
|
||||
|
||||
let conn = {
|
||||
let mut conn = None;
|
||||
|
@@ -49,7 +49,7 @@ impl FrozenClientRequest {
|
||||
where
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
RequestSender::Rc(self.head.clone(), None).send_body(
|
||||
RequestSender::Rc(Rc::clone(&self.head), None).send_body(
|
||||
self.addr,
|
||||
self.response_decompress,
|
||||
self.timeout,
|
||||
@@ -60,7 +60,7 @@ impl FrozenClientRequest {
|
||||
|
||||
/// Send a json body.
|
||||
pub fn send_json<T: Serialize>(&self, value: &T) -> SendClientRequest {
|
||||
RequestSender::Rc(self.head.clone(), None).send_json(
|
||||
RequestSender::Rc(Rc::clone(&self.head), None).send_json(
|
||||
self.addr,
|
||||
self.response_decompress,
|
||||
self.timeout,
|
||||
@@ -71,7 +71,7 @@ impl FrozenClientRequest {
|
||||
|
||||
/// Send an urlencoded body.
|
||||
pub fn send_form<T: Serialize>(&self, value: &T) -> SendClientRequest {
|
||||
RequestSender::Rc(self.head.clone(), None).send_form(
|
||||
RequestSender::Rc(Rc::clone(&self.head), None).send_form(
|
||||
self.addr,
|
||||
self.response_decompress,
|
||||
self.timeout,
|
||||
@@ -86,7 +86,7 @@ impl FrozenClientRequest {
|
||||
S: Stream<Item = Result<Bytes, E>> + 'static,
|
||||
E: Into<BoxError> + 'static,
|
||||
{
|
||||
RequestSender::Rc(self.head.clone(), None).send_stream(
|
||||
RequestSender::Rc(Rc::clone(&self.head), None).send_stream(
|
||||
self.addr,
|
||||
self.response_decompress,
|
||||
self.timeout,
|
||||
@@ -97,7 +97,7 @@ impl FrozenClientRequest {
|
||||
|
||||
/// Send an empty body.
|
||||
pub fn send(&self) -> SendClientRequest {
|
||||
RequestSender::Rc(self.head.clone(), None).send(
|
||||
RequestSender::Rc(Rc::clone(&self.head), None).send(
|
||||
self.addr,
|
||||
self.response_decompress,
|
||||
self.timeout,
|
||||
|
@@ -100,8 +100,6 @@
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||
#![warn(future_incompatible)]
|
||||
#![allow(unknown_lints)] // temp: #[allow(non_local_definitions)]
|
||||
#![allow(
|
||||
clippy::type_complexity,
|
||||
|
@@ -78,7 +78,7 @@ where
|
||||
RedirectServiceFuture::Tunnel { fut }
|
||||
}
|
||||
ConnectRequest::Client(head, body, addr) => {
|
||||
let connector = self.connector.clone();
|
||||
let connector = Rc::clone(&self.connector);
|
||||
let max_redirect_times = self.max_redirect_times;
|
||||
|
||||
// backup the uri and method for reuse schema and authority.
|
||||
|
Reference in New Issue
Block a user