Merge branch 'master' into master

This commit is contained in:
Rob Ede 2023-01-21 00:05:35 +00:00 committed by GitHub
commit 687be13084
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
80 changed files with 1110 additions and 221 deletions

View File

@ -5,6 +5,9 @@ on:
branches:
- master
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
check_benchmark:
runs-on: ubuntu-latest

View File

@ -4,6 +4,9 @@ on:
push:
branches: [master]
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
build_and_test_nightly:
strategy:
@ -92,29 +95,21 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Install stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable-x86_64-unknown-linux-gnu
profile: minimal
override: true
- uses: dtolnay/rust-toolchain@stable
- name: Install cargo-hack
uses: taiki-e/install-action@cargo-hack
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with: { command: generate-lockfile }
run: cargo generate-lockfile
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.2.0
- name: check feature combinations
uses: actions-rs/cargo@v1
with: { command: ci-check-all-feature-powerset }
run: cargo ci-check-all-feature-powerset
- name: check feature combinations
uses: actions-rs/cargo@v1
with: { command: ci-check-all-feature-powerset-linux }
run: cargo ci-check-all-feature-powerset-linux
nextest:
name: nextest
@ -127,24 +122,15 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
- uses: dtolnay/rust-toolchain@stable
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with: { command: generate-lockfile }
run: cargo generate-lockfile
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.3.0
- name: Test with cargo-nextest
uses: actions-rs/cargo@v1
with:
command: nextest
args: run
run: cargo nextest run

View File

@ -6,6 +6,9 @@ on:
push:
branches: [master]
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
build_and_test:
strategy:
@ -63,6 +66,11 @@ jobs:
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.2.0
- name: workaround MSRV issues
if: matrix.version != 'stable'
run: |
cargo update -p=zstd-sys --precise=2.0.1+zstd.1.5.2
- name: check minimal
uses: actions-rs/cargo@v1
with: { command: ci-check-min }
@ -96,16 +104,10 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable-x86_64-unknown-linux-gnu
profile: minimal
override: true
- uses: dtolnay/rust-toolchain@stable
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with: { command: generate-lockfile }
run: cargo generate-lockfile
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.3.0
@ -123,20 +125,13 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Install Rust (nightly)
uses: actions-rs/toolchain@v1
with:
toolchain: nightly-x86_64-unknown-linux-gnu
profile: minimal
override: true
- uses: dtolnay/rust-toolchain@nightly
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with: { command: generate-lockfile }
run: cargo generate-lockfile
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.3.0
- name: doc tests
uses: actions-rs/cargo@v1
run: cargo ci-doctest
timeout-minutes: 60
with: { command: ci-doctest }

View File

@ -9,54 +9,37 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
components: rustfmt
- name: Check with rustfmt
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- uses: dtolnay/rust-toolchain@nightly
with: { components: rustfmt }
- run: cargo fmt --all -- --check
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
components: clippy
override: true
- uses: dtolnay/rust-toolchain@stable
with: { components: clippy }
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with: { command: generate-lockfile }
run: cargo generate-lockfile
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.2.0
- name: Check with Clippy
uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --workspace --tests --examples --all-features
token: ${{ secrets.GITHUB_TOKEN }}
lint-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
components: rust-docs
- uses: dtolnay/rust-toolchain@stable
with: { components: rust-docs }
- name: Check for broken intra-doc links
uses: actions-rs/cargo@v1
env:

View File

@ -4,31 +4,29 @@ on:
push:
branches: [master]
permissions: {}
jobs:
build:
permissions:
contents: write # to push changes in repo (jamesives/github-pages-deploy-action)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: nightly-x86_64-unknown-linux-gnu
profile: minimal
override: true
- uses: dtolnay/rust-toolchain@nightly
- name: Build Docs
uses: actions-rs/cargo@v1
with:
command: doc
args: --workspace --all-features --no-deps
run: cargo +nightly doc --no-deps --workspace --all-features
env:
RUSTDOCFLAGS: --cfg=docsrs
- name: Tweak HTML
run: echo '<meta http-equiv="refresh" content="0;url=actix_web/index.html">' > target/doc/index.html
- name: Deploy to GitHub Pages
uses: JamesIves/github-pages-deploy-action@v4.4.0
uses: JamesIves/github-pages-deploy-action@v4.4.1
with:
folder: target/doc
single-commit: true

View File

@ -3,7 +3,6 @@ name = "actix-files"
version = "0.6.2"
authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"fakeshadow <24548779@qq.com>",
"Rob Ede <robjtede@icloud.com>",
]
description = "Static file serving for Actix Web"
@ -30,7 +29,7 @@ actix-web = { version = "4", default-features = false }
bitflags = "1"
bytes = "1"
derive_more = "0.99.5"
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
http-range = "0.1.4"
log = "0.4"
mime = "0.3"

View File

@ -13,6 +13,7 @@
#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible, missing_docs, missing_debug_implementations)]
#![allow(clippy::uninlined_format_args)]
use actix_service::boxed::{BoxService, BoxServiceFactory};
use actix_web::{

View File

@ -39,7 +39,7 @@ awc = { version = "3", default-features = false }
base64 = "0.13"
bytes = "1"
futures-core = { version = "0.3.7", default-features = false }
futures-core = { version = "0.3.17", default-features = false }
http = "0.2.5"
log = "0.4"
socket2 = "0.4"
@ -48,7 +48,7 @@ serde_json = "1.0"
slab = "0.4"
serde_urlencoded = "0.7"
tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
tokio = { version = "1.8.4", features = ["sync"] }
tokio = { version = "1.18.4", features = ["sync"] }
[dev-dependencies]
actix-web = { version = "4", default-features = false, features = ["cookies"] }

View File

@ -2,6 +2,7 @@
#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)]
#![allow(clippy::uninlined_format_args)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
@ -87,6 +88,7 @@ pub async fn test_server_with_addr<F: ServerServiceFactory<TcpStream>>(
// notify TestServer that server and system have shut down
// all thread managed resources should be dropped at this point
#[allow(clippy::let_underscore_future)]
let _ = thread_stop_tx.send(());
});
@ -294,6 +296,7 @@ impl Drop for TestServer {
// without needing to await anything
// signal server to stop
#[allow(clippy::let_underscore_future)]
let _ = self.server.stop(true);
// signal system to stop

View File

@ -5,14 +5,31 @@
- Fix non-empty body of http2 HEAD response.
### Added
- Implement `MessageBody` for `Cow<'static, str>` and `Cow<'static, [u8]>`. [#2959]
- Implement `MessageBody` for `&mut B` where `B: MessageBody + Unpin`. [#2868]
- Implement `MessageBody` for `Pin<B>` where `B::Target: MessageBody`. [#2868]
- Automatic h2c detection via new service finalizer `HttpService::tcp_auto_h2c()`. [#2957]
- `HeaderMap::retain()` [#2955].
- Header name constants in `header` module. [#2956] [#2968]
- `CACHE_STATUS`
- `CDN_CACHE_CONTROL`
- `CROSS_ORIGIN_EMBEDDER_POLICY`
- `CROSS_ORIGIN_OPENER_POLICY`
- `PERMISSIONS_POLICY`
- `X_FORWARDED_FOR`
- `X_FORWARDED_HOST`
- `X_FORWARDED_PROTO`
### Performance
- Improve overall performance of operations on `Extensions`. [#2890]
[#2959]: https://github.com/actix/actix-web/pull/2959
[#2868]: https://github.com/actix/actix-web/pull/2868
[#2890]: https://github.com/actix/actix-web/pull/2890
[#2957]: https://github.com/actix/actix-web/pull/2957
[#2955]: https://github.com/actix/actix-web/pull/2955
[#2956]: https://github.com/actix/actix-web/pull/2956
[#2968]: https://github.com/actix/actix-web/pull/2968
## 3.2.2 - 2022-09-11

View File

@ -67,7 +67,7 @@ bytes = "1"
bytestring = "1"
derive_more = "0.99.5"
encoding_rs = "0.8"
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
http = "0.2.5"
httparse = "1.5.1"
httpdate = "1.0.1"
@ -77,6 +77,8 @@ mime = "0.3"
percent-encoding = "2.1"
pin-project-lite = "0.2"
smallvec = "1.6.1"
tokio = { version = "1.18.4", features = [] }
tokio-util = { version = "0.7", features = ["io", "codec"] }
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
# http2
@ -94,7 +96,7 @@ actix-tls = { version = "3", default-features = false, optional = true }
# compress-*
brotli = { version = "3.3.3", optional = true }
flate2 = { version = "1.0.13", optional = true }
zstd = { version = "0.11", optional = true }
zstd = { version = "0.12", optional = true }
[dev-dependencies]
actix-http-test = { version = "3", features = ["openssl"] }
@ -103,9 +105,9 @@ actix-tls = { version = "3", features = ["openssl"] }
actix-web = "4"
async-stream = "0.3"
criterion = { version = "0.3", features = ["html_reports"] }
criterion = { version = "0.4", features = ["html_reports"] }
env_logger = "0.9"
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
memchr = "2.4"
once_cell = "1.9"
rcgen = "0.9"
@ -117,7 +119,7 @@ serde_json = "1.0"
static_assertions = "1"
tls-openssl = { package = "openssl", version = "0.10.9" }
tls-rustls = { package = "rustls", version = "0.20.0" }
tokio = { version = "1.8.4", features = ["net", "rt", "macros"] }
tokio = { version = "1.18.4", features = ["net", "rt", "macros"] }
[[example]]
name = "ws"

View File

@ -1,3 +1,5 @@
#![allow(clippy::uninlined_format_args)]
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
const CODES: &[u16] = &[0, 1000, 201, 800, 550];

View File

@ -0,0 +1,29 @@
//! An example that supports automatic selection of plaintext h1/h2c connections.
//!
//! Notably, both the following commands will work.
//! ```console
//! $ curl --http1.1 'http://localhost:8080/'
//! $ curl --http2-prior-knowledge 'http://localhost:8080/'
//! ```
use std::{convert::Infallible, io};
use actix_http::{HttpService, Request, Response, StatusCode};
use actix_server::Server;
#[tokio::main(flavor = "current_thread")]
async fn main() -> io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
Server::build()
.bind("h2c-detect", ("127.0.0.1", 8080), || {
HttpService::build()
.finish(|_req: Request| async move {
Ok::<_, Infallible>(Response::build(StatusCode::OK).body("Hello!"))
})
.tcp_auto_h2c()
})?
.workers(2)
.run()
.await
}

View File

@ -10,13 +10,13 @@ use std::{
time::Duration,
};
use actix_codec::Encoder;
use actix_http::{body::BodyStream, error::Error, ws, HttpService, Request, Response};
use actix_rt::time::{interval, Interval};
use actix_server::Server;
use bytes::{Bytes, BytesMut};
use bytestring::ByteString;
use futures_core::{ready, Stream};
use tokio_util::codec::Encoder;
use tracing::{info, trace};
#[actix_rt::main]

View File

@ -120,7 +120,7 @@ pub trait MessageBody {
}
mod foreign_impls {
use std::ops::DerefMut;
use std::{borrow::Cow, ops::DerefMut};
use super::*;
@ -324,6 +324,39 @@ mod foreign_impls {
}
}
impl MessageBody for Cow<'static, [u8]> {
type Error = Infallible;
#[inline]
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
#[inline]
fn poll_next(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
let bytes = match mem::take(self.get_mut()) {
Cow::Borrowed(b) => Bytes::from_static(b),
Cow::Owned(b) => Bytes::from(b),
};
Poll::Ready(Some(Ok(bytes)))
}
}
#[inline]
fn try_into_bytes(self) -> Result<Bytes, Self> {
match self {
Cow::Borrowed(b) => Ok(Bytes::from_static(b)),
Cow::Owned(b) => Ok(Bytes::from(b)),
}
}
}
impl MessageBody for &'static str {
type Error = Infallible;
@ -379,6 +412,39 @@ mod foreign_impls {
}
}
impl MessageBody for Cow<'static, str> {
type Error = Infallible;
#[inline]
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
#[inline]
fn poll_next(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
let bytes = match mem::take(self.get_mut()) {
Cow::Borrowed(s) => Bytes::from_static(s.as_bytes()),
Cow::Owned(s) => Bytes::from(s.into_bytes()),
};
Poll::Ready(Some(Ok(bytes)))
}
}
#[inline]
fn try_into_bytes(self) -> Result<Bytes, Self> {
match self {
Cow::Borrowed(s) => Ok(Bytes::from_static(s.as_bytes())),
Cow::Owned(s) => Ok(Bytes::from(s.into_bytes())),
}
}
}
impl MessageBody for bytestring::ByteString {
type Error = Infallible;

View File

@ -44,7 +44,7 @@ where
#[inline]
fn size(&self) -> BodySize {
BodySize::Sized(self.size as u64)
BodySize::Sized(self.size)
}
/// Attempts to pull out the next value of the underlying [`Stream`].

View File

@ -186,7 +186,7 @@ where
self
}
/// Finish service configuration and create a HTTP Service for HTTP/1 protocol.
/// Finish service configuration and create a service for the HTTP/1 protocol.
pub fn h1<F, B>(self, service: F) -> H1Service<T, S, B, X, U>
where
B: MessageBody,
@ -209,7 +209,7 @@ where
.on_connect_ext(self.on_connect_ext)
}
/// Finish service configuration and create a HTTP service for HTTP/2 protocol.
/// Finish service configuration and create a service for the HTTP/2 protocol.
#[cfg(feature = "http2")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
pub fn h2<F, B>(self, service: F) -> crate::h2::H2Service<T, S, B>

View File

@ -71,7 +71,7 @@ impl ChunkedState {
match size.checked_mul(radix) {
Some(n) => {
*size = n as u64;
*size = n;
*size += rem as u64;
Poll::Ready(Ok(ChunkedState::Size))

View File

@ -1,9 +1,9 @@
use std::{fmt, io};
use actix_codec::{Decoder, Encoder};
use bitflags::bitflags;
use bytes::{Bytes, BytesMut};
use http::{Method, Version};
use tokio_util::codec::{Decoder, Encoder};
use super::{
decoder::{self, PayloadDecoder, PayloadItem, PayloadType},

View File

@ -1,9 +1,9 @@
use std::{fmt, io};
use actix_codec::{Decoder, Encoder};
use bitflags::bitflags;
use bytes::BytesMut;
use http::{Method, Version};
use tokio_util::codec::{Decoder, Encoder};
use super::{
decoder::{self, PayloadDecoder, PayloadItem, PayloadType},

View File

@ -8,13 +8,15 @@ use std::{
task::{Context, Poll},
};
use actix_codec::{AsyncRead, AsyncWrite, Decoder as _, Encoder as _, Framed, FramedParts};
use actix_codec::{Framed, FramedParts};
use actix_rt::time::sleep_until;
use actix_service::Service;
use bitflags::bitflags;
use bytes::{Buf, BytesMut};
use futures_core::ready;
use pin_project_lite::pin_project;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio_util::codec::{Decoder as _, Encoder as _};
use tracing::{error, trace};
use crate::{
@ -1004,7 +1006,7 @@ where
this.read_buf.reserve(HW_BUFFER_SIZE - remaining);
}
match actix_codec::poll_read_buf(io.as_mut(), cx, this.read_buf) {
match tokio_util::io::poll_read_buf(io.as_mut(), cx, this.read_buf) {
Poll::Ready(Ok(n)) => {
this.flags.remove(Flags::FINISHED);

View File

@ -64,7 +64,7 @@ fn drop_payload_service(
fn echo_payload_service() -> impl Service<Request, Response = Response<Bytes>, Error = Error> {
fn_service(|mut req: Request| {
Box::pin(async move {
use futures_util::stream::StreamExt as _;
use futures_util::StreamExt as _;
let mut pl = req.take_payload();
let mut body = BytesMut::new();

View File

@ -450,7 +450,7 @@ impl TransferEncoding {
buf.extend_from_slice(&msg[..len as usize]);
*remaining -= len as u64;
*remaining -= len;
Ok(*remaining == 0)
} else {
Ok(true)

View File

@ -0,0 +1,51 @@
//! Common header names not defined in [`http`].
//!
//! Any headers added to this file will need to be re-exported from the list at `crate::headers`.
use http::header::HeaderName;
/// Response header field that indicates how caches have handled that response and its corresponding
/// request.
///
/// See [RFC 9211](https://www.rfc-editor.org/rfc/rfc9211) for full semantics.
pub const CACHE_STATUS: HeaderName = HeaderName::from_static("cache-status");
/// Response header field that allows origin servers to control the behavior of CDN caches
/// interposed between them and clients separately from other caches that might handle the response.
///
/// See [RFC 9213](https://www.rfc-editor.org/rfc/rfc9213) for full semantics.
pub const CDN_CACHE_CONTROL: HeaderName = HeaderName::from_static("cdn-cache-control");
/// Response header that prevents a document from loading any cross-origin resources that don't
/// explicitly grant the document permission (using [CORP] or [CORS]).
///
/// [CORP]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cross-Origin_Resource_Policy_(CORP)
/// [CORS]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
pub const CROSS_ORIGIN_EMBEDDER_POLICY: HeaderName =
HeaderName::from_static("cross-origin-embedder-policy");
/// Response header that allows you to ensure a top-level document does not share a browsing context
/// group with cross-origin documents.
pub const CROSS_ORIGIN_OPENER_POLICY: HeaderName =
HeaderName::from_static("cross-origin-opener-policy");
/// Response header that conveys a desire that the browser blocks no-cors cross-origin/cross-site
/// requests to the given resource.
pub const CROSS_ORIGIN_RESOURCE_POLICY: HeaderName =
HeaderName::from_static("cross-origin-resource-policy");
/// Response header that provides a mechanism to allow and deny the use of browser features in a
/// document or within any `<iframe>` elements in the document.
pub const PERMISSIONS_POLICY: HeaderName = HeaderName::from_static("permissions-policy");
/// Request header (de-facto standard) for identifying the originating IP address of a client
/// connecting to a web server through a proxy server.
pub const X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for");
/// Request header (de-facto standard) for identifying the original host requested by the client in
/// the `Host` HTTP request header.
pub const X_FORWARDED_HOST: HeaderName = HeaderName::from_static("x-forwarded-host");
/// Request header (de-facto standard) for identifying the protocol that a client used to connect to
/// your proxy or load balancer.
pub const X_FORWARDED_PROTO: HeaderName = HeaderName::from_static("x-forwarded-proto");

View File

@ -150,9 +150,7 @@ impl HeaderMap {
/// assert_eq!(map.len(), 3);
/// ```
pub fn len(&self) -> usize {
self.inner
.iter()
.fold(0, |acc, (_, values)| acc + values.len())
self.inner.values().map(|vals| vals.len()).sum()
}
/// Returns the number of _keys_ stored in the map.
@ -552,6 +550,39 @@ impl HeaderMap {
Keys(self.inner.keys())
}
/// Retains only the headers specified by the predicate.
///
/// In other words, removes all headers `(name, val)` for which `retain_fn(&name, &mut val)`
/// returns false.
///
/// The order in which headers are visited should be considered arbitrary.
///
/// # Examples
/// ```
/// # use actix_http::header::{self, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new();
///
/// map.append(header::HOST, HeaderValue::from_static("duck.com"));
/// map.append(header::SET_COOKIE, HeaderValue::from_static("one=1"));
/// map.append(header::SET_COOKIE, HeaderValue::from_static("two=2"));
///
/// map.retain(|name, val| val.as_bytes().starts_with(b"one"));
///
/// assert_eq!(map.len(), 1);
/// assert!(map.contains_key(&header::SET_COOKIE));
/// ```
pub fn retain<F>(&mut self, mut retain_fn: F)
where
F: FnMut(&HeaderName, &mut HeaderValue) -> bool,
{
self.inner.retain(|name, vals| {
vals.inner.retain(|val| retain_fn(name, val));
// invariant: make sure newly empty value lists are removed
!vals.is_empty()
})
}
/// Clears the map, returning all name-value sets as an iterator.
///
/// Header names will only be yielded for the first value in each set. All items that are
@ -943,6 +974,55 @@ mod tests {
assert!(map.is_empty());
}
#[test]
fn retain() {
let mut map = HeaderMap::new();
map.append(header::LOCATION, HeaderValue::from_static("/test"));
map.append(header::HOST, HeaderValue::from_static("duck.com"));
map.append(header::COOKIE, HeaderValue::from_static("one=1"));
map.append(header::COOKIE, HeaderValue::from_static("two=2"));
assert_eq!(map.len(), 4);
// by value
map.retain(|_, val| !val.as_bytes().contains(&b'/'));
assert_eq!(map.len(), 3);
// by name
map.retain(|name, _| name.as_str() != "cookie");
assert_eq!(map.len(), 1);
// keep but mutate value
map.retain(|_, val| {
*val = HeaderValue::from_static("replaced");
true
});
assert_eq!(map.len(), 1);
assert_eq!(map.get("host").unwrap(), "replaced");
}
#[test]
fn retain_removes_empty_value_lists() {
let mut map = HeaderMap::with_capacity(3);
map.append(header::HOST, HeaderValue::from_static("duck.com"));
map.append(header::HOST, HeaderValue::from_static("duck.com"));
assert_eq!(map.len(), 2);
assert_eq!(map.len_keys(), 1);
assert_eq!(map.inner.len(), 1);
assert_eq!(map.capacity(), 3);
// remove everything
map.retain(|_n, _v| false);
assert_eq!(map.len(), 0);
assert_eq!(map.len_keys(), 0);
assert_eq!(map.inner.len(), 0);
assert_eq!(map.capacity(), 3);
}
#[test]
fn entries_into_iter() {
let mut map = HeaderMap::new();

View File

@ -1,14 +1,18 @@
//! Pre-defined `HeaderName`s, traits for parsing and conversion, and other header utility methods.
// declaring new header consts will yield this error
#![allow(clippy::declare_interior_mutable_const)]
use percent_encoding::{AsciiSet, CONTROLS};
// re-export from http except header map related items
pub use http::header::{
pub use ::http::header::{
HeaderName, HeaderValue, InvalidHeaderName, InvalidHeaderValue, ToStrError,
};
// re-export const header names
pub use http::header::{
// re-export const header names, list is explicit so that any updates to `common` module do not
// conflict with this set
pub use ::http::header::{
ACCEPT, ACCEPT_CHARSET, ACCEPT_ENCODING, ACCEPT_LANGUAGE, ACCEPT_RANGES,
ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS,
ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_EXPOSE_HEADERS,
@ -30,22 +34,30 @@ pub use http::header::{
use crate::{error::ParseError, HttpMessage};
mod as_name;
mod common;
mod into_pair;
mod into_value;
pub mod map;
mod shared;
mod utils;
pub use self::as_name::AsHeaderName;
pub use self::into_pair::TryIntoHeaderPair;
pub use self::into_value::TryIntoHeaderValue;
pub use self::map::HeaderMap;
pub use self::shared::{
parse_extended_value, q, Charset, ContentEncoding, ExtendedValue, HttpDate, LanguageTag,
Quality, QualityItem,
pub use self::{
as_name::AsHeaderName,
into_pair::TryIntoHeaderPair,
into_value::TryIntoHeaderValue,
map::HeaderMap,
shared::{
parse_extended_value, q, Charset, ContentEncoding, ExtendedValue, HttpDate,
LanguageTag, Quality, QualityItem,
},
utils::{fmt_comma_delimited, from_comma_delimited, from_one_raw_str, http_percent_encode},
};
pub use self::utils::{
fmt_comma_delimited, from_comma_delimited, from_one_raw_str, http_percent_encode,
// re-export list is explicit so that any updates to `http` do not conflict with this set
pub use self::common::{
CACHE_STATUS, CDN_CACHE_CONTROL, CROSS_ORIGIN_EMBEDDER_POLICY, CROSS_ORIGIN_OPENER_POLICY,
CROSS_ORIGIN_RESOURCE_POLICY, PERMISSIONS_POLICY, X_FORWARDED_FOR, X_FORWARDED_HOST,
X_FORWARDED_PROTO,
};
/// An interface for types that already represent a valid header.

View File

@ -21,7 +21,8 @@
#![allow(
clippy::type_complexity,
clippy::too_many_arguments,
clippy::borrow_interior_mutable_const
clippy::borrow_interior_mutable_const,
clippy::uninlined_format_args
)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]

View File

@ -24,7 +24,39 @@ use crate::{
h1, ConnectCallback, OnConnectData, Protocol, Request, Response, ServiceConfig,
};
/// A `ServiceFactory` for HTTP/1.1 or HTTP/2 protocol.
/// A [`ServiceFactory`] for HTTP/1.1 and HTTP/2 connections.
///
/// Use [`build`](Self::build) to begin constructing service. Also see [`HttpServiceBuilder`].
///
/// # Automatic HTTP Version Selection
/// There are two ways to select the HTTP version of an incoming connection:
/// - One is to rely on the ALPN information that is provided when using a TLS (HTTPS); both
/// versions are supported automatically when using either of the `.rustls()` or `.openssl()`
/// finalizing methods.
/// - The other is to read the first few bytes of the TCP stream. This is the only viable approach
/// for supporting H2C, which allows the HTTP/2 protocol to work over plaintext connections. Use
/// the `.tcp_auto_h2c()` finalizing method to enable this behavior.
///
/// # Examples
/// ```
/// # use std::convert::Infallible;
/// use actix_http::{HttpService, Request, Response, StatusCode};
///
/// // this service would constructed in an actix_server::Server
///
/// # actix_rt::System::new().block_on(async {
/// HttpService::build()
/// // the builder finalizing method, other finalizers would not return an `HttpService`
/// .finish(|_req: Request| async move {
/// Ok::<_, Infallible>(
/// Response::build(StatusCode::OK).body("Hello!")
/// )
/// })
/// // the service finalizing method method
/// // you can use `.tcp_auto_h2c()`, `.rustls()`, or `.openssl()` instead of `.tcp()`
/// .tcp();
/// # })
/// ```
pub struct HttpService<T, S, B, X = h1::ExpectHandler, U = h1::UpgradeHandler> {
srv: S,
cfg: ServiceConfig,
@ -163,7 +195,9 @@ where
U::Error: fmt::Display + Into<Response<BoxBody>>,
U::InitError: fmt::Debug,
{
/// Create simple tcp stream service
/// Creates TCP stream service from HTTP service.
///
/// The resulting service only supports HTTP/1.x.
pub fn tcp(
self,
) -> impl ServiceFactory<
@ -179,6 +213,42 @@ where
})
.and_then(self)
}
/// Creates TCP stream service from HTTP service that automatically selects HTTP/1.x or HTTP/2
/// on plaintext connections.
#[cfg(feature = "http2")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
pub fn tcp_auto_h2c(
self,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Response = (),
Error = DispatchError,
InitError = (),
> {
fn_service(move |io: TcpStream| async move {
// subset of HTTP/2 preface defined by RFC 9113 §3.4
// this subset was chosen to maximize likelihood that peeking only once will allow us to
// reliably determine version or else it should fallback to h1 and fail quickly if data
// on the wire is junk
const H2_PREFACE: &[u8] = b"PRI * HTTP/2";
let mut buf = [0; 12];
io.peek(&mut buf).await?;
let proto = if buf == H2_PREFACE {
Protocol::Http2
} else {
Protocol::Http1
};
let peer_addr = io.peer_addr().ok();
Ok((io, proto, peer_addr))
})
.and_then(self)
}
}
/// Configuration options used when accepting TLS connection.

View File

@ -1,7 +1,7 @@
use actix_codec::{Decoder, Encoder};
use bitflags::bitflags;
use bytes::{Bytes, BytesMut};
use bytestring::ByteString;
use tokio_util::codec::{Decoder, Encoder};
use tracing::error;
use super::{

View File

@ -76,7 +76,9 @@ mod inner {
use pin_project_lite::pin_project;
use tracing::debug;
use actix_codec::{AsyncRead, AsyncWrite, Decoder, Encoder, Framed};
use actix_codec::Framed;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio_util::codec::{Decoder, Encoder};
use crate::{body::BoxBody, Response};

View File

@ -1,4 +1,5 @@
#![cfg(feature = "openssl")]
#![allow(clippy::uninlined_format_args)]
extern crate tls_openssl as openssl;
@ -16,7 +17,7 @@ use actix_utils::future::{err, ok, ready};
use bytes::{Bytes, BytesMut};
use derive_more::{Display, Error};
use futures_core::Stream;
use futures_util::stream::{once, StreamExt as _};
use futures_util::{stream::once, StreamExt as _};
use openssl::{
pkey::PKey,
ssl::{SslAcceptor, SslMethod},

View File

@ -1,4 +1,5 @@
#![cfg(feature = "rustls")]
#![allow(clippy::uninlined_format_args)]
extern crate tls_rustls as rustls;

View File

@ -1,3 +1,5 @@
#![allow(clippy::uninlined_format_args)]
use std::{
convert::Infallible,
io::{Read, Write},
@ -7,18 +9,15 @@ use std::{
use actix_http::{
body::{self, BodyStream, BoxBody, SizedStream},
header, Error, HttpService, KeepAlive, Request, Response, StatusCode,
header, Error, HttpService, KeepAlive, Request, Response, StatusCode, Version,
};
use actix_http_test::test_server;
use actix_rt::time::sleep;
use actix_rt::{net::TcpStream, time::sleep};
use actix_service::fn_service;
use actix_utils::future::{err, ok, ready};
use bytes::Bytes;
use derive_more::{Display, Error};
use futures_util::{
stream::{once, StreamExt as _},
FutureExt as _,
};
use futures_util::{stream::once, FutureExt as _, StreamExt as _};
use regex::Regex;
#[actix_rt::test]
@ -858,3 +857,44 @@ async fn not_modified_spec_h1() {
srv.stop().await;
}
#[actix_rt::test]
async fn h2c_auto() {
let mut srv = test_server(|| {
HttpService::build()
.keep_alive(KeepAlive::Disabled)
.finish(|req: Request| {
let body = match req.version() {
Version::HTTP_11 => "h1",
Version::HTTP_2 => "h2",
_ => unreachable!(),
};
ok::<_, Infallible>(Response::ok().set_body(body))
})
.tcp_auto_h2c()
})
.await;
let req = srv.get("/");
assert_eq!(req.get_version(), &Version::HTTP_11);
let mut res = req.send().await.unwrap();
assert!(res.status().is_success());
assert_eq!(res.body().await.unwrap(), &b"h1"[..]);
// awc doesn't support forcing the version to http/2 so use h2 manually
let tcp = TcpStream::connect(srv.addr()).await.unwrap();
let (h2, connection) = h2::client::handshake(tcp).await.unwrap();
tokio::spawn(async move { connection.await.unwrap() });
let mut h2 = h2.ready().await.unwrap();
let request = ::http::Request::new(());
let (response, _) = h2.send_request(request, true).unwrap();
let (head, mut body) = response.await.unwrap().into_parts();
let body = body.data().await.unwrap().unwrap();
assert!(head.status.is_success());
assert_eq!(body, &b"h2"[..]);
srv.stop().await;
}

View File

@ -1,3 +1,5 @@
#![allow(clippy::uninlined_format_args)]
use std::{
cell::Cell,
convert::Infallible,

View File

@ -19,7 +19,7 @@ actix-web = { version = "4", default-features = false }
bytes = "1"
derive_more = "0.99.5"
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
httparse = "1.3"
local-waker = "0.1"
log = "0.4"
@ -29,6 +29,6 @@ memchr = "2.5"
[dev-dependencies]
actix-rt = "2.2"
actix-http = "3"
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
tokio = { version = "1.8.4", features = ["sync"] }
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
tokio = { version = "1.18.4", features = ["sync"] }
tokio-stream = "0.1"

View File

@ -14,7 +14,7 @@ use crate::server::Multipart;
/// ```
/// use actix_web::{web, HttpResponse, Error};
/// use actix_multipart::Multipart;
/// use futures_util::stream::StreamExt as _;
/// use futures_util::StreamExt as _;
///
/// async fn index(mut payload: Multipart) -> Result<HttpResponse, Error> {
/// // iterate over multipart stream

View File

@ -2,7 +2,7 @@
#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)]
#![allow(clippy::borrow_interior_mutable_const)]
#![allow(clippy::borrow_interior_mutable_const, clippy::uninlined_format_args)]
mod error;
mod extractor;

View File

@ -868,7 +868,7 @@ mod tests {
use actix_web::test::TestRequest;
use actix_web::FromRequest;
use bytes::Bytes;
use futures_util::{future::lazy, StreamExt};
use futures_util::{future::lazy, StreamExt as _};
use std::time::Duration;
use tokio::sync::mpsc;
use tokio_stream::wrappers::UnboundedReceiverStream;

View File

@ -27,7 +27,7 @@ serde = "1"
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
[dev-dependencies]
criterion = { version = "0.3", features = ["html_reports"] }
criterion = { version = "0.4", features = ["html_reports"] }
http = "0.2.5"
serde = { version = "1", features = ["derive"] }
percent-encoding = "2.1"

View File

@ -1,3 +1,5 @@
#![allow(clippy::uninlined_format_args)]
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use std::borrow::Cow;

View File

@ -2,6 +2,7 @@
#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)]
#![allow(clippy::uninlined_format_args)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]

View File

@ -37,12 +37,12 @@ actix-utils = "3"
actix-web = { version = "4", default-features = false, features = ["cookies"] }
awc = { version = "3", default-features = false, features = ["cookies"] }
futures-core = { version = "0.3.7", default-features = false, features = ["std"] }
futures-util = { version = "0.3.7", default-features = false, features = [] }
futures-core = { version = "0.3.17", default-features = false, features = ["std"] }
futures-util = { version = "0.3.17", default-features = false, features = [] }
log = "0.4"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_urlencoded = "0.7"
tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
tls-rustls = { package = "rustls", version = "0.20.0", optional = true }
tokio = { version = "1.8.4", features = ["sync"] }
tokio = { version = "1.18.4", features = ["sync"] }

View File

@ -321,6 +321,7 @@ where
// all thread managed resources should be dropped at this point
});
#[allow(clippy::let_underscore_future)]
let _ = thread_stop_tx.send(());
});
@ -567,6 +568,7 @@ impl Drop for TestServer {
// without needing to await anything
// signal server to stop
#[allow(clippy::let_underscore_future)]
let _ = self.server.stop(true);
// signal system to stop

View File

@ -21,9 +21,10 @@ actix-web = { version = "4", default-features = false }
bytes = "1"
bytestring = "1"
futures-core = { version = "0.3.7", default-features = false }
futures-core = { version = "0.3.17", default-features = false }
pin-project-lite = "0.2"
tokio = { version = "1.13.1", features = ["sync"] }
tokio = { version = "1.18.4", features = ["sync"] }
tokio-util = { version = "0.7", features = ["codec"] }
[dev-dependencies]
actix-rt = "2.2"
@ -34,4 +35,4 @@ actix-web = { version = "4", features = ["macros"] }
mime = "0.3"
env_logger = "0.9"
futures-util = { version = "0.3.7", default-features = false }
futures-util = { version = "0.3.17", default-features = false }

View File

@ -57,6 +57,7 @@
#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)]
#![allow(clippy::uninlined_format_args)]
mod context;
pub mod ws;

View File

@ -74,7 +74,6 @@ use actix::{
Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, Message as ActixMessage,
SpawnHandle,
};
use actix_codec::{Decoder as _, Encoder as _};
use actix_http::ws::{hash_key, Codec};
pub use actix_http::ws::{
CloseCode, CloseReason, Frame, HandshakeError, Message, ProtocolError,
@ -92,6 +91,7 @@ use bytestring::ByteString;
use futures_core::Stream;
use pin_project_lite::pin_project;
use tokio::sync::oneshot;
use tokio_util::codec::{Decoder as _, Encoder as _};
/// Builder for Websocket session response.
///

View File

@ -3,7 +3,7 @@ use actix_http::ws::Codec;
use actix_web::{web, App, HttpRequest};
use actix_web_actors::ws;
use bytes::Bytes;
use futures_util::{SinkExt, StreamExt};
use futures_util::{SinkExt as _, StreamExt as _};
struct Ws;

View File

@ -27,6 +27,6 @@ actix-test = "0.1"
actix-utils = "3"
actix-web = "4"
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
trybuild = "1"
rustversion = "1"

View File

@ -155,7 +155,7 @@ impl Args {
if !methods.insert(method) {
return Err(syn::Error::new_spanned(
&nv.lit,
&format!(
format!(
"HTTP method defined more than once: `{}`",
lit.value()
),

View File

@ -5,11 +5,20 @@
- Add `ContentDisposition::attachment` constructor. [#2867]
- Add `ErrorHandlers::default_handler()` (as well as `default_handler_{server, client}()`) to make registering handlers for groups of response statuses easier. [#2784]
- Add `Logger::custom_response_replace()`. [#2631]
- Add rudimentary redirection service at `web::redirect()` / `web::Redirect`. [#1961]
- Add `guard::Acceptable` for matching against `Accept` header mime types. [#2265]
- Add fallible versions of test helpers: `try_call_service`, `try_call_and_read_body_json`, `try_read_body`, and `try_read_body_json`. [#2961]
### Fixed
- Add `Allow` header to `Resource`'s default responses when no routes are matched. [#2949]
[#1961]: https://github.com/actix/actix-web/pull/1961
[#2265]: https://github.com/actix/actix-web/pull/2265
[#2631]: https://github.com/actix/actix-web/pull/2631
[#2784]: https://github.com/actix/actix-web/pull/2784
[#2867]: https://github.com/actix/actix-web/pull/2867
[#2949]: https://github.com/actix/actix-web/pull/2949
[#2961]: https://github.com/actix/actix-web/pull/2961
## 4.2.1 - 2022-09-12
### Fixed

View File

@ -38,10 +38,7 @@ compress-gzip = ["actix-http/compress-gzip", "__compress"]
compress-zstd = ["actix-http/compress-zstd", "__compress"]
# Routing and runtime proc macros
macros = [
"actix-macros",
"actix-web-codegen",
]
macros = ["actix-macros", "actix-web-codegen"]
# Cookies support
cookies = ["cookie"]
@ -82,8 +79,8 @@ cfg-if = "1"
cookie = { version = "0.16", features = ["percent-encode"], optional = true }
derive_more = "0.99.8"
encoding_rs = "0.8"
futures-core = { version = "0.3.7", default-features = false }
futures-util = { version = "0.3.7", default-features = false }
futures-core = { version = "0.3.17", default-features = false }
futures-util = { version = "0.3.17", default-features = false }
http = "0.2.8"
itoa = "1"
language-tags = "0.3"
@ -107,10 +104,10 @@ awc = { version = "3", features = ["openssl"] }
brotli = "3.3.3"
const-str = "0.4"
criterion = { version = "0.3", features = ["html_reports"] }
criterion = { version = "0.4", features = ["html_reports"] }
env_logger = "0.9"
flate2 = "1.0.13"
futures-util = { version = "0.3.7", default-features = false, features = ["std"] }
futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
rand = "0.8"
rcgen = "0.9"
rustls-pemfile = "1"
@ -118,8 +115,8 @@ serde = { version = "1.0", features = ["derive"] }
static_assertions = "1"
tls-openssl = { package = "openssl", version = "0.10.9" }
tls-rustls = { package = "rustls", version = "0.20.0" }
tokio = { version = "1.13.1", features = ["rt-multi-thread", "macros"] }
zstd = "0.11"
tokio = { version = "1.18.4", features = ["rt-multi-thread", "macros"] }
zstd = "0.12"
[[test]]
name = "test_server"

View File

@ -1,3 +1,5 @@
#![allow(clippy::uninlined_format_args)]
use actix_web::{web, App, HttpResponse};
use awc::Client;
use criterion::{criterion_group, criterion_main, Criterion};

View File

@ -1,3 +1,5 @@
#![allow(clippy::uninlined_format_args)]
use actix_web::{get, middleware, web, App, HttpRequest, HttpResponse, HttpServer};
#[get("/resource1/{name}/index.html")]

View File

@ -1,3 +1,5 @@
#![allow(clippy::uninlined_format_args)]
use actix_web::{middleware, rt, web, App, HttpRequest, HttpServer};
async fn index(req: HttpRequest) -> &'static str {

View File

@ -4,6 +4,8 @@
//! For an example of extracting a client TLS certificate, see:
//! <https://github.com/actix/examples/tree/master/https-tls/rustls-client-cert>
#![allow(clippy::uninlined_format_args)]
use std::{any::Any, io, net::SocketAddr};
use actix_web::{

View File

@ -1,3 +1,5 @@
#![allow(clippy::uninlined_format_args)]
use actix_web::{get, web, HttpRequest};
#[cfg(unix)]
use actix_web::{middleware, App, Error, HttpResponse, HttpServer};

View File

@ -5,7 +5,7 @@ use actix_service::{
apply, apply_fn_factory, boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt,
Transform,
};
use futures_util::future::FutureExt as _;
use futures_util::FutureExt as _;
use crate::{
app_service::{AppEntry, AppInit, AppRoutingFactory},
@ -712,6 +712,7 @@ mod tests {
.route("/", web::to(|| async { "hello" }))
}
#[allow(clippy::let_underscore_future)]
let _ = init_service(my_app());
}
}

View File

@ -0,0 +1,99 @@
use super::{Guard, GuardContext};
use crate::http::header::Accept;
/// A guard that verifies that an `Accept` header is present and it contains a compatible MIME type.
///
/// An exception is that matching `*/*` must be explicitly enabled because most browsers send this
/// as part of their `Accept` header for almost every request.
///
/// # Examples
/// ```
/// use actix_web::{guard::Acceptable, web, HttpResponse};
///
/// web::resource("/images")
/// .guard(Acceptable::new(mime::IMAGE_STAR))
/// .default_service(web::to(|| async {
/// HttpResponse::Ok().body("only called when images responses are acceptable")
/// }));
/// ```
#[derive(Debug, Clone)]
pub struct Acceptable {
mime: mime::Mime,
/// Wether to match `*/*` mime type.
///
/// Defaults to false because it's not very useful otherwise.
match_star_star: bool,
}
impl Acceptable {
/// Constructs new `Acceptable` guard with the given `mime` type/pattern.
pub fn new(mime: mime::Mime) -> Self {
Self {
mime,
match_star_star: false,
}
}
/// Allows `*/*` in the `Accept` header to pass the guard check.
pub fn match_star_star(mut self) -> Self {
self.match_star_star = true;
self
}
}
impl Guard for Acceptable {
fn check(&self, ctx: &GuardContext<'_>) -> bool {
let accept = match ctx.header::<Accept>() {
Some(hdr) => hdr,
None => return false,
};
let target_type = self.mime.type_();
let target_subtype = self.mime.subtype();
for mime in accept.0.into_iter().map(|q| q.item) {
return match (mime.type_(), mime.subtype()) {
(typ, subtype) if typ == target_type && subtype == target_subtype => true,
(typ, mime::STAR) if typ == target_type => true,
(mime::STAR, mime::STAR) if self.match_star_star => true,
_ => continue,
};
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{http::header, test::TestRequest};
#[test]
fn test_acceptable() {
let req = TestRequest::default().to_srv_request();
assert!(!Acceptable::new(mime::APPLICATION_JSON).check(&req.guard_ctx()));
let req = TestRequest::default()
.insert_header((header::ACCEPT, "application/json"))
.to_srv_request();
assert!(Acceptable::new(mime::APPLICATION_JSON).check(&req.guard_ctx()));
let req = TestRequest::default()
.insert_header((header::ACCEPT, "text/html, application/json"))
.to_srv_request();
assert!(Acceptable::new(mime::APPLICATION_JSON).check(&req.guard_ctx()));
}
#[test]
fn test_acceptable_star() {
let req = TestRequest::default()
.insert_header((header::ACCEPT, "text/html, */*;q=0.8"))
.to_srv_request();
assert!(Acceptable::new(mime::APPLICATION_JSON)
.match_star_star()
.check(&req.guard_ctx()));
}
}

View File

@ -56,6 +56,9 @@ use actix_http::{header, uri::Uri, Extensions, Method as HttpMethod, RequestHead
use crate::{http::header::Header, service::ServiceRequest, HttpMessage as _};
mod acceptable;
pub use self::acceptable::Acceptable;
/// Provides access to request parts that are useful during routing.
#[derive(Debug)]
pub struct GuardContext<'a> {
@ -193,6 +196,7 @@ impl AnyGuard {
}
impl Guard for AnyGuard {
#[inline]
fn check(&self, ctx: &GuardContext<'_>) -> bool {
for guard in &self.guards {
if guard.check(ctx) {
@ -244,12 +248,14 @@ impl AllGuard {
}
impl Guard for AllGuard {
#[inline]
fn check(&self, ctx: &GuardContext<'_>) -> bool {
for guard in &self.guards {
if !guard.check(ctx) {
return false;
}
}
true
}
}
@ -268,6 +274,7 @@ impl Guard for AllGuard {
pub struct Not<G>(pub G);
impl<G: Guard> Guard for Not<G> {
#[inline]
fn check(&self, ctx: &GuardContext<'_>) -> bool {
!self.0.check(ctx)
}
@ -279,11 +286,25 @@ pub fn Method(method: HttpMethod) -> impl Guard {
MethodGuard(method)
}
#[derive(Debug, Clone)]
pub(crate) struct RegisteredMethods(pub(crate) Vec<HttpMethod>);
/// HTTP method guard.
struct MethodGuard(HttpMethod);
#[derive(Debug)]
pub(crate) struct MethodGuard(HttpMethod);
impl Guard for MethodGuard {
fn check(&self, ctx: &GuardContext<'_>) -> bool {
let registered = ctx.req_data_mut().remove::<RegisteredMethods>();
if let Some(mut methods) = registered {
methods.0.push(self.0.clone());
ctx.req_data_mut().insert(methods);
} else {
ctx.req_data_mut()
.insert(RegisteredMethods(vec![self.0.clone()]));
}
ctx.head().method == self.0
}
}

View File

@ -6,8 +6,7 @@ use super::{common_header, QualityItem};
use crate::http::header;
common_header! {
/// `Accept` header, defined
/// in [RFC 7231 §5.3.2](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2)
/// `Accept` header, defined in [RFC 7231 §5.3.2].
///
/// The `Accept` header field can be used by user agents to specify
/// response media types that are acceptable. Accept header fields can
@ -71,6 +70,8 @@ common_header! {
/// ])
/// );
/// ```
///
/// [RFC 7231 §5.3.2]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
(Accept, header::ACCEPT) => (QualityItem<Mime>)*
test_parse_and_format {
@ -106,8 +107,7 @@ common_header! {
test4,
vec![b"text/plain; charset=utf-8; q=0.5"],
Some(Accept(vec![
QualityItem::new(mime::TEXT_PLAIN_UTF_8,
q(0.5)),
QualityItem::new(mime::TEXT_PLAIN_UTF_8, q(0.5)),
])));
#[test]

View File

@ -69,6 +69,7 @@
#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)]
#![allow(clippy::uninlined_format_args)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_cfg))]
@ -86,6 +87,7 @@ mod helpers;
pub mod http;
mod info;
pub mod middleware;
mod redirect;
mod request;
mod request_data;
mod resource;

View File

@ -7,7 +7,7 @@ use std::{
};
use futures_core::{future::LocalBoxFuture, ready};
use futures_util::future::FutureExt as _;
use futures_util::FutureExt as _;
use pin_project_lite::pin_project;
use crate::{

View File

@ -351,7 +351,7 @@ mod tests {
use actix_service::IntoService;
use actix_utils::future::ok;
use bytes::Bytes;
use futures_util::future::FutureExt as _;
use futures_util::FutureExt as _;
use super::*;
use crate::{

238
actix-web/src/redirect.rs Normal file
View File

@ -0,0 +1,238 @@
//! See [`Redirect`] for service/responder documentation.
use std::borrow::Cow;
use actix_utils::future::ready;
use crate::{
dev::{fn_service, AppService, HttpServiceFactory, ResourceDef, ServiceRequest},
http::{header::LOCATION, StatusCode},
HttpRequest, HttpResponse, Responder,
};
/// An HTTP service for redirecting one path to another path or URL.
///
/// By default, the "307 Temporary Redirect" status is used when responding. See [this MDN
/// article][mdn-redirects] on why 307 is preferred over 302.
///
/// # Examples
/// As service:
/// ```
/// use actix_web::{web, App};
///
/// App::new()
/// // redirect "/duck" to DuckDuckGo
/// .service(web::redirect("/duck", "https://duck.com"))
/// .service(
/// // redirect "/api/old" to "/api/new"
/// web::scope("/api").service(web::redirect("/old", "/new"))
/// );
/// ```
///
/// As responder:
/// ```
/// use actix_web::{web::Redirect, Responder};
///
/// async fn handler() -> impl Responder {
/// // sends a permanent (308) redirect to duck.com
/// Redirect::to("https://duck.com").permanent()
/// }
/// # actix_web::web::to(handler);
/// ```
///
/// [mdn-redirects]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#temporary_redirections
#[derive(Debug, Clone)]
pub struct Redirect {
from: Cow<'static, str>,
to: Cow<'static, str>,
status_code: StatusCode,
}
impl Redirect {
/// Construct a new `Redirect` service that matches a path.
///
/// This service will match exact paths equal to `from` within the current scope. I.e., when
/// registered on the root `App`, it will match exact, whole paths. But when registered on a
/// `Scope`, it will match paths under that scope, ignoring the defined scope prefix, just like
/// a normal `Resource` or `Route`.
///
/// The `to` argument can be path or URL; whatever is provided shall be used verbatim when
/// setting the redirect location. This means that relative paths can be used to navigate
/// relatively to matched paths.
///
/// Prefer [`Redirect::to()`](Self::to) when using `Redirect` as a responder since `from` has
/// no meaning in that context.
///
/// # Examples
/// ```
/// # use actix_web::{web::Redirect, App};
/// App::new()
/// // redirects "/oh/hi/mark" to "/oh/bye/johnny"
/// .service(Redirect::new("/oh/hi/mark", "../../bye/johnny"));
/// ```
pub fn new(from: impl Into<Cow<'static, str>>, to: impl Into<Cow<'static, str>>) -> Self {
Self {
from: from.into(),
to: to.into(),
status_code: StatusCode::TEMPORARY_REDIRECT,
}
}
/// Construct a new `Redirect` to use as a responder.
///
/// Only receives the `to` argument since responders do not need to do route matching.
///
/// # Examples
/// ```
/// use actix_web::{web::Redirect, Responder};
///
/// async fn admin_page() -> impl Responder {
/// // sends a temporary 307 redirect to the login path
/// Redirect::to("/login")
/// }
/// # actix_web::web::to(admin_page);
/// ```
pub fn to(to: impl Into<Cow<'static, str>>) -> Self {
Self {
from: "/".into(),
to: to.into(),
status_code: StatusCode::TEMPORARY_REDIRECT,
}
}
/// Use the "308 Permanent Redirect" status when responding.
///
/// See [this MDN article][mdn-redirects] on why 308 is preferred over 301.
///
/// [mdn-redirects]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#permanent_redirections
pub fn permanent(self) -> Self {
self.using_status_code(StatusCode::PERMANENT_REDIRECT)
}
/// Use the "307 Temporary Redirect" status when responding.
///
/// See [this MDN article][mdn-redirects] on why 307 is preferred over 302.
///
/// [mdn-redirects]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#temporary_redirections
pub fn temporary(self) -> Self {
self.using_status_code(StatusCode::TEMPORARY_REDIRECT)
}
/// Use the "303 See Other" status when responding.
///
/// This status code is semantically correct as the response to a successful login, for example.
pub fn see_other(self) -> Self {
self.using_status_code(StatusCode::SEE_OTHER)
}
/// Allows the use of custom status codes for less common redirect types.
///
/// In most cases, the default status ("308 Permanent Redirect") or using the `temporary`
/// method, which uses the "307 Temporary Redirect" status have more consistent behavior than
/// 301 and 302 codes, respectively.
///
/// ```
/// # use actix_web::{http::StatusCode, web::Redirect};
/// // redirects would use "301 Moved Permanently" status code
/// Redirect::new("/old", "/new")
/// .using_status_code(StatusCode::MOVED_PERMANENTLY);
///
/// // redirects would use "302 Found" status code
/// Redirect::new("/old", "/new")
/// .using_status_code(StatusCode::FOUND);
/// ```
pub fn using_status_code(mut self, status: StatusCode) -> Self {
self.status_code = status;
self
}
}
impl HttpServiceFactory for Redirect {
fn register(self, config: &mut AppService) {
let redirect = self.clone();
let rdef = ResourceDef::new(self.from.into_owned());
let redirect_factory = fn_service(move |mut req: ServiceRequest| {
let res = redirect.clone().respond_to(req.parts_mut().0);
ready(Ok(req.into_response(res.map_into_boxed_body())))
});
config.register_service(rdef, None, redirect_factory, None)
}
}
impl Responder for Redirect {
type Body = ();
fn respond_to(self, _req: &HttpRequest) -> HttpResponse<Self::Body> {
let mut res = HttpResponse::with_body(self.status_code, ());
if let Ok(hdr_val) = self.to.parse() {
res.headers_mut().insert(LOCATION, hdr_val);
} else {
log::error!(
"redirect target location can not be converted to header value: {:?}",
self.to
);
}
res
}
}
#[cfg(test)]
mod tests {
use crate::{dev::Service, http::StatusCode, test, App};
use super::*;
#[actix_rt::test]
async fn absolute_redirects() {
let redirector = Redirect::new("/one", "/two").permanent();
let svc = test::init_service(App::new().service(redirector)).await;
let req = test::TestRequest::default().uri("/one").to_request();
let res = svc.call(req).await.unwrap();
assert_eq!(res.status(), StatusCode::from_u16(308).unwrap());
let hdr = res.headers().get(&LOCATION).unwrap();
assert_eq!(hdr.to_str().unwrap(), "/two");
}
#[actix_rt::test]
async fn relative_redirects() {
let redirector = Redirect::new("/one", "two").permanent();
let svc = test::init_service(App::new().service(redirector)).await;
let req = test::TestRequest::default().uri("/one").to_request();
let res = svc.call(req).await.unwrap();
assert_eq!(res.status(), StatusCode::from_u16(308).unwrap());
let hdr = res.headers().get(&LOCATION).unwrap();
assert_eq!(hdr.to_str().unwrap(), "two");
}
#[actix_rt::test]
async fn temporary_redirects() {
let external_service = Redirect::new("/external", "https://duck.com");
let svc = test::init_service(App::new().service(external_service)).await;
let req = test::TestRequest::default().uri("/external").to_request();
let res = svc.call(req).await.unwrap();
assert_eq!(res.status(), StatusCode::from_u16(307).unwrap());
let hdr = res.headers().get(&LOCATION).unwrap();
assert_eq!(hdr.to_str().unwrap(), "https://duck.com");
}
#[actix_rt::test]
async fn as_responder() {
let responder = Redirect::to("https://duck.com");
let req = test::TestRequest::default().to_http_request();
let res = responder.respond_to(&req);
assert_eq!(res.status(), StatusCode::from_u16(307).unwrap());
let hdr = res.headers().get(&LOCATION).unwrap();
assert_eq!(hdr.to_str().unwrap(), "https://duck.com");
}
}

View File

@ -13,8 +13,9 @@ use crate::{
body::MessageBody,
data::Data,
dev::{ensure_leading_slash, AppService, ResourceDef},
guard::Guard,
guard::{self, Guard},
handler::Handler,
http::header,
route::{Route, RouteService},
service::{
BoxedHttpService, BoxedHttpServiceFactory, HttpServiceFactory, ServiceRequest,
@ -40,8 +41,11 @@ use crate::{
/// .route(web::get().to(|| HttpResponse::Ok())));
/// ```
///
/// If no matching route could be found, *405* response code get returned. Default behavior could be
/// overridden with `default_resource()` method.
/// If no matching route is found, [a 405 response is returned with an appropriate Allow header][RFC
/// 9110 §15.5.6]. This default behavior can be overridden using
/// [`default_service()`](Self::default_service).
///
/// [RFC 9110 §15.5.6]: https://www.rfc-editor.org/rfc/rfc9110.html#section-15.5.6
pub struct Resource<T = ResourceEndpoint> {
endpoint: T,
rdef: Patterns,
@ -66,7 +70,19 @@ impl Resource {
guards: Vec::new(),
app_data: None,
default: boxed::factory(fn_service(|req: ServiceRequest| async {
use crate::HttpMessage as _;
let allowed = req.extensions().get::<guard::RegisteredMethods>().cloned();
if let Some(methods) = allowed {
Ok(req.into_response(
HttpResponse::MethodNotAllowed()
.insert_header(header::Allow(methods.0))
.finish(),
))
} else {
Ok(req.into_response(HttpResponse::MethodNotAllowed()))
}
})),
}
}
@ -309,13 +325,28 @@ where
}
}
/// Default service to be used if no matching route could be found.
/// Sets the default service to be used if no matching route is found.
///
/// You can use a [`Route`] as default service.
/// Unlike [`Scope`]s, a `Resource` does _not_ inherit its parent's default service. You can
/// use a [`Route`] as default service.
///
/// If a default service is not registered, an empty `405 Method Not Allowed` response will be
/// sent to the client instead. Unlike [`Scope`](crate::Scope)s, a [`Resource`] does **not**
/// inherit its parent's default service.
/// If a custom default service is not registered, an empty `405 Method Not Allowed` response
/// with an appropriate Allow header will be sent instead.
///
/// # Examples
/// ```
/// use actix_web::{App, HttpResponse, web};
///
/// let resource = web::resource("/test")
/// .route(web::get().to(HttpResponse::Ok))
/// .default_service(web::to(|| {
/// HttpResponse::BadRequest()
/// }));
///
/// App::new().service(resource);
/// ```
///
/// [`Scope`]: crate::Scope
pub fn default_service<F, U>(mut self, f: F) -> Self
where
F: IntoServiceFactory<U, ServiceRequest>,
@ -606,7 +637,11 @@ mod tests {
async fn test_default_resource() {
let srv = init_service(
App::new()
.service(web::resource("/test").route(web::get().to(HttpResponse::Ok)))
.service(
web::resource("/test")
.route(web::get().to(HttpResponse::Ok))
.route(web::delete().to(HttpResponse::Ok)),
)
.default_service(|r: ServiceRequest| {
ok(r.into_response(HttpResponse::BadRequest()))
}),
@ -621,6 +656,10 @@ mod tests {
.to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
assert_eq!(
resp.headers().get(header::ALLOW).unwrap().as_bytes(),
b"GET, DELETE"
);
let srv = init_service(
App::new().service(

View File

@ -10,12 +10,16 @@
//! # Calling Test Service
//! - [`TestRequest`]
//! - [`call_service`]
//! - [`try_call_service`]
//! - [`call_and_read_body`]
//! - [`call_and_read_body_json`]
//! - [`try_call_and_read_body_json`]
//!
//! # Reading Response Payloads
//! - [`read_body`]
//! - [`try_read_body`]
//! - [`read_body_json`]
//! - [`try_read_body_json`]
// TODO: more docs on generally how testing works with these parts
@ -31,7 +35,8 @@ pub use self::test_services::{default_service, ok_service, simple_service, statu
#[allow(deprecated)]
pub use self::test_utils::{
call_and_read_body, call_and_read_body_json, call_service, init_service, read_body,
read_body_json, read_response, read_response_json,
read_body_json, read_response, read_response_json, try_call_and_read_body_json,
try_call_service, try_read_body, try_read_body_json,
};
#[cfg(test)]

View File

@ -100,6 +100,15 @@ where
.expect("test service call returned error")
}
/// Fallible version of [`call_service`] that allows testing response completion errors.
pub async fn try_call_service<S, R, B, E>(app: &S, req: R) -> Result<S::Response, E>
where
S: Service<R, Response = ServiceResponse<B>, Error = E>,
E: std::fmt::Debug,
{
app.call(req).await
}
/// Helper function that returns a response body of a TestRequest
///
/// # Examples
@ -185,13 +194,23 @@ pub async fn read_body<B>(res: ServiceResponse<B>) -> Bytes
where
B: MessageBody,
{
let body = res.into_body();
body::to_bytes(body)
try_read_body(res)
.await
.map_err(Into::<Box<dyn StdError>>::into)
.expect("error reading test response body")
}
/// Fallible version of [`read_body`] that allows testing MessageBody reading errors.
pub async fn try_read_body<B>(
res: ServiceResponse<B>,
) -> Result<Bytes, <B as MessageBody>::Error>
where
B: MessageBody,
{
let body = res.into_body();
body::to_bytes(body).await
}
/// Helper function that returns a deserialized response body of a ServiceResponse.
///
/// # Examples
@ -240,18 +259,27 @@ where
B: MessageBody,
T: DeserializeOwned,
{
let body = read_body(res).await;
serde_json::from_slice(&body).unwrap_or_else(|err| {
try_read_body_json(res).await.unwrap_or_else(|err| {
panic!(
"could not deserialize body into a {}\nerr: {}\nbody: {:?}",
"could not deserialize body into a {}\nerr: {}",
std::any::type_name::<T>(),
err,
body,
)
})
}
/// Fallible version of [`read_body_json`] that allows testing response deserialzation errors.
pub async fn try_read_body_json<T, B>(res: ServiceResponse<B>) -> Result<T, Box<dyn StdError>>
where
B: MessageBody,
T: DeserializeOwned,
{
let body = try_read_body(res)
.await
.map_err(Into::<Box<dyn StdError>>::into)?;
serde_json::from_slice(&body).map_err(Into::<Box<dyn StdError>>::into)
}
/// Helper function that returns a deserialized response body of a TestRequest
///
/// # Examples
@ -299,8 +327,23 @@ where
B: MessageBody,
T: DeserializeOwned,
{
let res = call_service(app, req).await;
read_body_json(res).await
try_call_and_read_body_json(app, req).await.unwrap()
}
/// Fallible version of [`call_and_read_body_json`] that allows testing service call errors.
pub async fn try_call_and_read_body_json<S, B, T>(
app: &S,
req: Request,
) -> Result<T, Box<dyn StdError>>
where
S: Service<Request, Response = ServiceResponse<B>, Error = Error>,
B: MessageBody,
T: DeserializeOwned,
{
let res = try_call_service(app, req)
.await
.map_err(Into::<Box<dyn StdError>>::into)?;
try_read_body_json(res).await
}
#[doc(hidden)]
@ -358,7 +401,7 @@ mod tests {
assert_eq!(result, Bytes::from_static(b"delete!"));
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Debug)]
pub struct Person {
id: String,
name: String,
@ -383,6 +426,26 @@ mod tests {
assert_eq!(&result.id, "12345");
}
#[actix_rt::test]
async fn test_try_response_json_error() {
let app = init_service(App::new().service(web::resource("/people").route(
web::post().to(|person: web::Json<Person>| HttpResponse::Ok().json(person)),
)))
.await;
let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes();
let req = TestRequest::post()
.uri("/animals") // Not registered to ensure an error occurs.
.insert_header((header::CONTENT_TYPE, "application/json"))
.set_payload(payload)
.to_request();
let result: Result<Person, Box<dyn StdError>> =
try_call_and_read_body_json(&app, req).await;
assert!(result.is_err());
}
#[actix_rt::test]
async fn test_body_json() {
let app = init_service(App::new().service(web::resource("/people").route(
@ -403,6 +466,27 @@ mod tests {
assert_eq!(&result.name, "User name");
}
#[actix_rt::test]
async fn test_try_body_json_error() {
let app = init_service(App::new().service(web::resource("/people").route(
web::post().to(|person: web::Json<Person>| HttpResponse::Ok().json(person)),
)))
.await;
// Use a number for id to cause a deserialization error.
let payload = r#"{"id":12345,"name":"User name"}"#.as_bytes();
let res = TestRequest::post()
.uri("/people")
.insert_header((header::CONTENT_TYPE, "application/json"))
.set_payload(payload)
.send_request(&app)
.await;
let result: Result<Person, Box<dyn StdError>> = try_read_body_json(res).await;
assert!(result.is_err());
}
#[actix_rt::test]
async fn test_request_response_form() {
let app = init_service(App::new().service(web::resource("/people").route(

View File

@ -35,6 +35,7 @@ use crate::{
///
/// Use [`FormConfig`] to configure extraction options.
///
/// ## Examples
/// ```
/// use actix_web::{post, web};
/// use serde::Deserialize;
@ -46,20 +47,18 @@ use crate::{
///
/// // This handler is only called if:
/// // - request headers declare the content type as `application/x-www-form-urlencoded`
/// // - request payload is deserialized into a `Info` struct from the URL encoded format
/// // - request payload deserializes into an `Info` struct from the URL encoded format
/// #[post("/")]
/// async fn index(form: web::Form<Info>) -> String {
/// async fn index(web::Form(form): web::Form<Info>) -> String {
/// format!("Welcome {}!", form.name)
/// }
/// ```
///
/// # Responder
/// The `Form` type also allows you to create URL encoded responses:
/// simply return a value of type Form<T> where T is the type to be URL encoded.
/// The type must implement [`serde::Serialize`].
///
/// Responses use
/// The `Form` type also allows you to create URL encoded responses by returning a value of type
/// `Form<T>` where `T` is the type to be URL encoded, as long as `T` implements [`Serialize`].
///
/// ## Examples
/// ```
/// use actix_web::{get, web};
/// use serde::Serialize;
@ -77,7 +76,7 @@ use crate::{
/// #[get("/")]
/// async fn index() -> web::Form<SomeForm> {
/// web::Form(SomeForm {
/// name: "actix".into(),
/// name: "actix".to_owned(),
/// age: 123
/// })
/// }

View File

@ -27,7 +27,7 @@ use crate::{
/// # Examples
/// ```
/// use std::future::Future;
/// use futures_util::stream::StreamExt as _;
/// use futures_util::StreamExt as _;
/// use actix_web::{post, web};
///
/// // `body: web::Payload` parameter extracts raw payload stream from request

View File

@ -177,7 +177,7 @@ where
#[cfg(test)]
mod tests {
use futures_util::stream::StreamExt as _;
use futures_util::StreamExt as _;
use super::*;
use crate::test::TestRequest;

View File

@ -11,10 +11,12 @@
//! - [`Bytes`]: Raw payload
//!
//! # Responders
//! - [`Json`]: JSON request payload
//! - [`Bytes`]: Raw request payload
//! - [`Json`]: JSON response
//! - [`Form`]: URL-encoded response
//! - [`Bytes`]: Raw bytes response
//! - [`Redirect`](Redirect::to): Convenient redirect responses
use std::future::Future;
use std::{borrow::Cow, future::Future};
use actix_router::IntoPatterns;
pub use bytes::{Buf, BufMut, Bytes, BytesMut};
@ -26,6 +28,7 @@ use crate::{
pub use crate::config::ServiceConfig;
pub use crate::data::Data;
pub use crate::redirect::Redirect;
pub use crate::request_data::ReqData;
pub use crate::types::*;
@ -45,6 +48,7 @@ pub use crate::types::*;
/// For instance, to route `GET`-requests on any route matching `/users/{userid}/{friend}` and store
/// `userid` and `friend` in the exposed `Path` object:
///
/// # Examples
/// ```
/// use actix_web::{web, App, HttpResponse};
///
@ -74,6 +78,7 @@ pub fn resource<T: IntoPatterns>(path: T) -> Resource {
/// - `/{project_id}/path2`
/// - `/{project_id}/path3`
///
/// # Examples
/// ```
/// use actix_web::{web, App, HttpResponse};
///
@ -183,6 +188,25 @@ pub fn service<T: IntoPatterns>(path: T) -> WebService {
WebService::new(path)
}
/// Create a relative or absolute redirect.
///
/// See [`Redirect`] docs for usage details.
///
/// # Examples
/// ```
/// use actix_web::{web, App};
///
/// let app = App::new()
/// // the client will resolve this redirect to /api/to-path
/// .service(web::redirect("/api/from-path", "to-path"));
/// ```
pub fn redirect(
from: impl Into<Cow<'static, str>>,
to: impl Into<Cow<'static, str>>,
) -> Redirect {
Redirect::new(from, to)
}
/// Executes blocking function on a thread pool, returns future that resolves to result of the
/// function execution.
pub fn block<F, R>(f: F) -> impl Future<Output = Result<R, BlockingError>>

View File

@ -1,3 +1,5 @@
#![allow(clippy::uninlined_format_args)]
#[cfg(feature = "openssl")]
extern crate tls_openssl as openssl;

View File

@ -1,10 +1,7 @@
[package]
name = "awc"
version = "3.0.1"
authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"fakeshadow <24548779@qq.com>",
]
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Async HTTP and WebSocket client library"
keywords = ["actix", "http", "framework", "async", "web"]
categories = [
@ -70,8 +67,8 @@ base64 = "0.13"
bytes = "1"
cfg-if = "1"
derive_more = "0.99.5"
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] }
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
futures-util = { version = "0.3.17", default-features = false, features = ["alloc", "sink"] }
h2 = "0.3.9"
http = "0.2.5"
itoa = "1"
@ -83,14 +80,14 @@ rand = "0.8"
serde = "1.0"
serde_json = "1.0"
serde_urlencoded = "0.7"
tokio = { version = "1.8.4", features = ["sync"] }
tokio = { version = "1.18.4", features = ["sync"] }
cookie = { version = "0.16", features = ["percent-encode"], optional = true }
tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features = ["dangerous_configuration"] }
trust-dns-resolver = { version = "0.21", optional = true }
trust-dns-resolver = { version = "0.22", optional = true }
[dev-dependencies]
actix-http = { version = "3", features = ["openssl"] }
@ -105,12 +102,12 @@ brotli = "3.3.3"
const-str = "0.4"
env_logger = "0.9"
flate2 = "1.0.13"
futures-util = { version = "0.3.7", default-features = false }
futures-util = { version = "0.3.17", default-features = false }
static_assertions = "1.1"
rcgen = "0.9"
rustls-pemfile = "1"
tokio = { version = "1.13.1", features = ["rt-multi-thread", "macros"] }
zstd = "0.11"
tokio = { version = "1.18.4", features = ["rt-multi-thread", "macros"] }
zstd = "0.12"
[[example]]
name = "client"

View File

@ -1,3 +1,5 @@
#![allow(clippy::uninlined_format_args)]
use std::error::Error as StdError;
#[tokio::main]

View File

@ -210,7 +210,7 @@ where
};
self.add_default_header((
header::AUTHORIZATION,
format!("Basic {}", base64::encode(&auth)),
format!("Basic {}", base64::encode(auth)),
))
}

View File

@ -19,7 +19,7 @@ use actix_rt::time::{sleep, Sleep};
use actix_service::Service;
use ahash::AHashMap;
use futures_core::future::LocalBoxFuture;
use futures_util::FutureExt;
use futures_util::FutureExt as _;
use http::uri::Authority;
use pin_project_lite::pin_project;
use tokio::sync::{OwnedSemaphorePermit, Semaphore};

View File

@ -83,7 +83,7 @@
//! ```no_run
//! # #[actix_rt::main]
//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! use futures_util::{sink::SinkExt as _, stream::StreamExt as _};
//! use futures_util::{SinkExt as _, StreamExt as _};
//!
//! let (_resp, mut connection) = awc::Client::new()
//! .ws("ws://echo.websocket.org")
@ -105,7 +105,8 @@
#![allow(
clippy::type_complexity,
clippy::borrow_interior_mutable_const,
clippy::needless_doctest_main
clippy::needless_doctest_main,
clippy::uninlined_format_args
)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]

View File

@ -238,7 +238,7 @@ impl ClientRequest {
self.insert_header((
header::AUTHORIZATION,
format!("Basic {}", base64::encode(&auth)),
format!("Basic {}", base64::encode(auth)),
))
}
@ -565,6 +565,8 @@ mod tests {
assert_eq!(req.head.version, Version::HTTP_2);
let _ = req.headers_mut();
#[allow(clippy::let_underscore_future)]
let _ = req.send_body("");
}

View File

@ -6,7 +6,7 @@
//!
//! ```no_run
//! use awc::{Client, ws};
//! use futures_util::{sink::SinkExt as _, stream::StreamExt as _};
//! use futures_util::{SinkExt as _, StreamExt as _};
//!
//! #[actix_rt::main]
//! async fn main() {
@ -236,7 +236,7 @@ impl WebsocketsRequest {
Some(password) => format!("{}:{}", username, password),
None => format!("{}:", username),
};
self.header(AUTHORIZATION, format!("Basic {}", base64::encode(&auth)))
self.header(AUTHORIZATION, format!("Basic {}", base64::encode(auth)))
}
/// Set HTTP bearer authentication header
@ -503,6 +503,8 @@ mod tests {
.unwrap(),
"Bearer someS3cr3tAutht0k3n"
);
#[allow(clippy::let_underscore_future)]
let _ = req.connect();
}

View File

@ -1,3 +1,5 @@
#![allow(clippy::uninlined_format_args)]
use std::{
collections::HashMap,
convert::Infallible,
@ -139,7 +141,7 @@ async fn timeout_override() {
#[actix_rt::test]
async fn response_timeout() {
use futures_util::stream::{once, StreamExt as _};
use futures_util::{stream::once, StreamExt as _};
let srv = actix_test::start(|| {
App::new().service(web::resource("/").route(web::to(|| async {