1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-07-06 19:00:18 +02:00

Compare commits

..

39 Commits

Author SHA1 Message Date
232a14dc8b prepare actix-files release 0.6.0-beta.15 2022-01-21 20:27:29 +00:00
6e9f5fba24 prepare awc release 3.0.0-beta.19 2022-01-21 20:25:46 +00:00
c5d6df0078 prepare actix-web release 4.0.0-beta.21 2022-01-21 20:23:29 +00:00
8865540f3b prepare actix-http release 3.0.0-beta.19 2022-01-21 20:21:49 +00:00
141790b200 use camel case in special headers
fixes #2595
2022-01-21 20:15:43 +00:00
9668a2396f prepare actix-router release 0.5.0-rc.2 2022-01-21 17:21:46 +00:00
cb7347216c add Logger::log_target (#2594) 2022-01-21 17:19:17 +00:00
ae7f71e317 remove ambiguous HttpResponseBuilder::del_cookie (#2591) 2022-01-21 17:18:07 +00:00
bc89f0bfc2 s/example/examples 2022-01-21 16:56:33 +00:00
c959916346 fmt codegen 2022-01-20 01:54:57 +00:00
f227e880d7 refactor route codegen to be cleaner 2022-01-20 01:53:02 +00:00
68ad81f989 remove debug logs 2022-01-20 01:30:33 +00:00
f2e736719a add url_for test for conflicting named resources 2022-01-20 01:30:33 +00:00
81ef12a0fd add warn log to from_parts if given request is cloned
closes #2562
2022-01-19 22:23:53 +00:00
1bc1538118 use tokio::main in client example 2022-01-19 21:36:14 +00:00
1cc3e7b24c deprecate Path::path (#2590) 2022-01-19 20:26:33 +00:00
3dd98c308c document Path::unprocessed panic 2022-01-19 18:33:23 +00:00
cb5d9a7e64 bump deps to stable actix-server v2 2022-01-19 16:58:11 +00:00
5ee555462f add HttpResponse::add_removal_cookie (#2586) 2022-01-19 16:36:11 +00:00
ad159f5219 fix ClientResponse::body doc
fixes #2589
2022-01-19 15:52:16 +00:00
2ffc21dd4f move response extensions out of head (#2585) 2022-01-19 02:09:25 +00:00
7b8a392ef5 allow camel case response headers (#2587) 2022-01-16 03:16:26 +00:00
3c7ccf5521 update http changelog 2022-01-15 15:43:18 +00:00
e7cae5a95b migrate to brotli crate (#2538) 2022-01-15 14:03:16 +00:00
455d5c460d prepare actix-files release 0.6.0-beta.14 2022-01-14 20:01:11 +00:00
8faca783fa prepare actix-web release 4.0.0-beta.20 2022-01-14 20:00:26 +00:00
edbb9b047e prepare actix-router release 0.5.0-rc.1 2022-01-14 19:59:36 +00:00
32742d0715 support opaque app in test helpers (#2584) 2022-01-14 19:45:32 +00:00
d90c1a2331 convert error in Result extractor (#2581)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-01-12 18:59:22 +00:00
2a12b41456 fix support for 12 extractors (#2582)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-01-12 18:31:48 +00:00
6c97d448b7 update tokio-uring to 0.2 (#2583) 2022-01-12 17:53:36 +00:00
c3ce33df05 unify generics across App, Scope and Resource (#2572)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-01-05 15:02:28 +00:00
4431c8da65 fix bench 2022-01-05 14:10:38 +00:00
2d11ab5977 Add ServiceConfig::configure (#1988)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-01-05 12:31:39 +00:00
4ebf16890d add GuardContext::header (#2569) 2022-01-05 11:47:14 +00:00
fe0bbfb3da optimize PathDeserializer (#2570) 2022-01-05 10:48:20 +00:00
2462b6dd5d generalize impl Responder for HttpResponse (#2567)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-01-05 04:42:52 +00:00
49cfabeaf5 simplify Resource trait (#2568)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-01-05 04:34:13 +00:00
0f7292c69a remove readme msrv link 2022-01-05 04:24:40 +00:00
86 changed files with 1560 additions and 1090 deletions

View File

@ -3,6 +3,41 @@
## Unreleased - 2021-xx-xx
## 4.0.0-beta.21 - 2022-01-21
### Added
- `HttpResponse::add_removal_cookie`. [#2586]
- `Logger::log_target`. [#2594]
### Removed
- `HttpRequest::req_data[_mut]()`; request-local data is still available through `.extensions()`. [#2585]
- `HttpRequestBuilder::del_cookie`. [#2591]
[#2585]: https://github.com/actix/actix-web/pull/2585
[#2586]: https://github.com/actix/actix-web/pull/2586
[#2591]: https://github.com/actix/actix-web/pull/2591
[#2594]: https://github.com/actix/actix-web/pull/2594
## 4.0.0-beta.20 - 2022-01-14
### Added
- `GuardContext::header` [#2569]
- `ServiceConfig::configure` to allow easy nesting of configuration functions. [#1988]
### Changed
- `HttpResponse` can now be used as a `Responder` with any body type. [#2567]
- `Result` extractor wrapper can now convert error types. [#2581]
- Associated types in `FromRequest` impl for `Option` and `Result` has changed. [#2581]
- Maximum number of handler extractors has increased to 12. [#2582]
- Removed bound `<B as MessageBody>::Error: Debug` in test utility functions in order to support returning opaque apps. [#2584]
[#1988]: https://github.com/actix/actix-web/pull/1988
[#2567]: https://github.com/actix/actix-web/pull/2567
[#2569]: https://github.com/actix/actix-web/pull/2569
[#2581]: https://github.com/actix/actix-web/pull/2581
[#2582]: https://github.com/actix/actix-web/pull/2582
[#2584]: https://github.com/actix/actix-web/pull/2584
## 4.0.0-beta.19 - 2022-01-04
### Added
- `impl Hash` for `http::header::Encoding`. [#2501]

View File

@ -1,7 +1,10 @@
[package]
name = "actix-web"
version = "4.0.0-beta.19"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
version = "4.0.0-beta.21"
authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>",
]
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
keywords = ["actix", "http", "web", "framework", "async"]
categories = [
@ -71,14 +74,14 @@ experimental-io-uring = ["actix-server/io-uring"]
[dependencies]
actix-codec = "0.4.1"
actix-macros = "0.2.3"
actix-rt = "2.3"
actix-server = "2.0.0-rc.2"
actix-rt = "2.6"
actix-server = "2"
actix-service = "2.0.0"
actix-utils = "3.0.0"
actix-tls = { version = "3.0.0", default-features = false, optional = true }
actix-http = "3.0.0-beta.18"
actix-router = "0.5.0-beta.4"
actix-http = "3.0.0-beta.19"
actix-router = "0.5.0-rc.2"
actix-web-codegen = "0.5.0-rc.1"
ahash = "0.7"
@ -105,11 +108,11 @@ time = { version = "0.3", default-features = false, features = ["formatting"] }
url = "2.1"
[dev-dependencies]
actix-files = "0.6.0-beta.13"
actix-files = "0.6.0-beta.15"
actix-test = { version = "0.1.0-beta.11", features = ["openssl", "rustls"] }
awc = { version = "3.0.0-beta.18", features = ["openssl"] }
awc = { version = "3.0.0-beta.19", features = ["openssl"] }
brotli2 = "0.3.2"
brotli = "3.3.3"
const-str = "0.3"
criterion = { version = "0.3", features = ["html_reports"] }
env_logger = "0.9"
@ -118,6 +121,7 @@ futures-util = { version = "0.3.7", default-features = false, features = ["std"]
rand = "0.8"
rcgen = "0.8"
rustls-pemfile = "0.2"
static_assertions = "1"
tls-openssl = { package = "openssl", version = "0.10.9" }
tls-rustls = { package = "rustls", version = "0.20.0" }
zstd = "0.9"
@ -156,6 +160,10 @@ awc = { path = "awc" }
name = "test_server"
required-features = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"]
[[test]]
name = "compression"
required-features = ["compress-brotli", "compress-gzip", "compress-zstd"]
[[example]]
name = "basic"
required-features = ["compress-gzip"]

View File

@ -6,13 +6,13 @@
<p>
[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web)
[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.19)](https://docs.rs/actix-web/4.0.0-beta.19)
[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.21)](https://docs.rs/actix-web/4.0.0-beta.21)
![MSRV](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg)
[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.19/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.19)
[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.21/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.21)
<br />
[![CI](https://github.com/actix/actix-web/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-web/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web)
[![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web)
![downloads](https://img.shields.io/crates/d/actix-web.svg)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -3,6 +3,16 @@
## Unreleased - 2021-xx-xx
## 0.6.0-beta.15 - 2022-01-21
- No significant changes since `0.6.0-beta.14`.
## 0.6.0-beta.14 - 2022-01-14
- The `prefer_utf8` option introduced in `0.4.0` is now true by default. [#2583]
[#2583]: https://github.com/actix/actix-web/pull/2583
## 0.6.0-beta.13 - 2022-01-04
- The `Files` service now rejects requests with URL paths that include `%2F` (decoded: `/`). [#2398]
- The `Files` service now correctly decodes `%25` in the URL path to `%` for the file path. [#2398]

View File

@ -1,6 +1,6 @@
[package]
name = "actix-files"
version = "0.6.0-beta.13"
version = "0.6.0-beta.15"
authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"fakeshadow <24548779@qq.com>",
@ -22,10 +22,10 @@ path = "src/lib.rs"
experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"]
[dependencies]
actix-http = "3.0.0-beta.18"
actix-http = "3.0.0-beta.19"
actix-service = "2"
actix-utils = "3"
actix-web = { version = "4.0.0-beta.19", default-features = false }
actix-web = { version = "4.0.0-beta.21", default-features = false }
askama_escape = "0.10"
bitflags = "1"
@ -39,10 +39,10 @@ mime_guess = "2.0.1"
percent-encoding = "2.1"
pin-project-lite = "0.2.7"
tokio-uring = { version = "0.1", optional = true }
tokio-uring = { version = "0.2", optional = true, features = ["bytes"] }
[dev-dependencies]
actix-rt = "2.2"
actix-test = "0.1.0-beta.11"
actix-web = "4.0.0-beta.19"
actix-web = "4.0.0-beta.21"
tempfile = "3.2"

View File

@ -3,11 +3,11 @@
> Static file serving for Actix Web
[![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files)
[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.13)](https://docs.rs/actix-files/0.6.0-beta.13)
[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.15)](https://docs.rs/actix-files/0.6.0-beta.15)
[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
![License](https://img.shields.io/crates/l/actix-files.svg)
<br />
[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.13/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.13)
[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.15/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.15)
[![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -10,6 +10,9 @@ use actix_web::{error::Error, web::Bytes};
use futures_core::{ready, Stream};
use pin_project_lite::pin_project;
#[cfg(feature = "experimental-io-uring")]
use bytes::BytesMut;
use super::named::File;
pin_project! {
@ -214,64 +217,3 @@ where
}
}
}
#[cfg(feature = "experimental-io-uring")]
use bytes_mut::BytesMut;
// TODO: remove new type and use bytes::BytesMut directly
#[doc(hidden)]
#[cfg(feature = "experimental-io-uring")]
mod bytes_mut {
use std::ops::{Deref, DerefMut};
use tokio_uring::buf::{IoBuf, IoBufMut};
#[derive(Debug)]
pub struct BytesMut(bytes::BytesMut);
impl BytesMut {
pub(super) fn new() -> Self {
Self(bytes::BytesMut::new())
}
}
impl Deref for BytesMut {
type Target = bytes::BytesMut;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for BytesMut {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
unsafe impl IoBuf for BytesMut {
fn stable_ptr(&self) -> *const u8 {
self.0.as_ptr()
}
fn bytes_init(&self) -> usize {
self.0.len()
}
fn bytes_total(&self) -> usize {
self.0.capacity()
}
}
unsafe impl IoBufMut for BytesMut {
fn stable_mut_ptr(&mut self) -> *mut u8 {
self.0.as_mut_ptr()
}
unsafe fn set_init(&mut self, init_len: usize) {
if self.len() < init_len {
self.0.set_len(init_len);
}
}
}
}

View File

@ -2,7 +2,7 @@
//!
//! Provides a non-blocking service for serving static files from disk.
//!
//! # Example
//! # Examples
//! ```
//! use actix_web::App;
//! use actix_files::Files;
@ -67,8 +67,8 @@ mod tests {
time::{Duration, SystemTime},
};
use actix_service::ServiceFactory;
use actix_web::{
dev::ServiceFactory,
guard,
http::{
header::{self, ContentDisposition, DispositionParam, DispositionType},
@ -303,7 +303,7 @@ mod tests {
let resp = file.respond_to(&req).await.unwrap();
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
"application/javascript"
"application/javascript; charset=utf-8"
);
assert_eq!(
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),

View File

@ -1,15 +1,16 @@
use std::{
fmt,
fs::Metadata,
io,
path::{Path, PathBuf},
time::{SystemTime, UNIX_EPOCH},
};
use actix_service::{Service, ServiceFactory};
use actix_web::{
body::{self, BoxBody, SizedStream},
dev::{AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse},
dev::{
self, AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory,
ServiceRequest, ServiceResponse,
},
http::{
header::{
self, Charset, ContentDisposition, ContentEncoding, DispositionParam,
@ -37,7 +38,7 @@ bitflags! {
impl Default for Flags {
fn default() -> Self {
Flags::from_bits_truncate(0b0000_0111)
Flags::from_bits_truncate(0b0000_1111)
}
}
@ -65,12 +66,12 @@ impl Default for Flags {
/// NamedFile::open_async("./static/index.html").await
/// }
/// ```
#[derive(Deref, DerefMut)]
#[derive(Debug, Deref, DerefMut)]
pub struct NamedFile {
path: PathBuf,
#[deref]
#[deref_mut]
file: File,
path: PathBuf,
modified: Option<SystemTime>,
pub(crate) md: Metadata,
pub(crate) flags: Flags,
@ -80,32 +81,6 @@ pub struct NamedFile {
pub(crate) encoding: Option<ContentEncoding>,
}
impl fmt::Debug for NamedFile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("NamedFile")
.field("path", &self.path)
.field(
"file",
#[cfg(feature = "experimental-io-uring")]
{
&"tokio_uring::File"
},
#[cfg(not(feature = "experimental-io-uring"))]
{
&self.file
},
)
.field("modified", &self.modified)
.field("md", &self.md)
.field("flags", &self.flags)
.field("status_code", &self.status_code)
.field("content_type", &self.content_type)
.field("content_disposition", &self.content_disposition)
.field("encoding", &self.encoding)
.finish()
}
}
#[cfg(not(feature = "experimental-io-uring"))]
pub(crate) use std::fs::File;
#[cfg(feature = "experimental-io-uring")]
@ -627,7 +602,7 @@ impl Service<ServiceRequest> for NamedFileService {
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
actix_service::always_ready!();
dev::always_ready!();
fn call(&self, req: ServiceRequest) -> Self::Future {
let (req, _) = req.into_parts();

View File

@ -85,7 +85,7 @@ impl FromRequest for PathBufWrap {
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
ready(req.match_info().path().parse())
ready(req.match_info().unprocessed().parse())
}
}

View File

@ -2,7 +2,7 @@ use std::{fmt, io, ops::Deref, path::PathBuf, rc::Rc};
use actix_web::{
body::BoxBody,
dev::{Service, ServiceRequest, ServiceResponse},
dev::{self, Service, ServiceRequest, ServiceResponse},
error::Error,
guard::Guard,
http::{header, Method},
@ -98,7 +98,7 @@ impl Service<ServiceRequest> for FilesService {
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
actix_service::always_ready!();
dev::always_ready!();
fn call(&self, req: ServiceRequest) -> Self::Future {
let is_method_valid = if let Some(guard) = &self.guards {
@ -120,14 +120,16 @@ impl Service<ServiceRequest> for FilesService {
));
}
let real_path =
match PathBufWrap::parse_path(req.match_info().path(), this.hidden_files) {
Ok(item) => item,
Err(err) => return Ok(req.error_response(err)),
};
let path_on_disk = match PathBufWrap::parse_path(
req.match_info().unprocessed(),
this.hidden_files,
) {
Ok(item) => item,
Err(err) => return Ok(req.error_response(err)),
};
if let Some(filter) = &this.path_filter {
if !filter(real_path.as_ref(), req.head()) {
if !filter(path_on_disk.as_ref(), req.head()) {
if let Some(ref default) = this.default {
return default.call(req).await;
} else {
@ -137,7 +139,7 @@ impl Service<ServiceRequest> for FilesService {
}
// full file path
let path = this.directory.join(&real_path);
let path = this.directory.join(&path_on_disk);
if let Err(err) = path.canonicalize() {
return this.handle_err(err, req).await;
}

View File

@ -19,12 +19,12 @@ async fn test_utf8_file_contents() {
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get(header::CONTENT_TYPE),
Some(&HeaderValue::from_static("text/plain")),
Some(&HeaderValue::from_static("text/plain; charset=utf-8")),
);
// prefer UTF-8 encoding
// disable UTF-8 attribute
let srv =
test::init_service(App::new().service(Files::new("/", "./tests").prefer_utf8(true)))
test::init_service(App::new().service(Files::new("/", "./tests").prefer_utf8(false)))
.await;
let req = TestRequest::with_uri("/utf8.txt").to_request();
@ -33,6 +33,6 @@ async fn test_utf8_file_contents() {
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get(header::CONTENT_TYPE),
Some(&HeaderValue::from_static("text/plain; charset=utf-8")),
Some(&HeaderValue::from_static("text/plain")),
);
}

View File

@ -34,8 +34,8 @@ actix-codec = "0.4.1"
actix-tls = "3.0.0"
actix-utils = "3.0.0"
actix-rt = "2.2"
actix-server = "2.0.0-rc.2"
awc = { version = "3.0.0-beta.18", default-features = false }
actix-server = "2"
awc = { version = "3.0.0-beta.19", default-features = false }
base64 = "0.13"
bytes = "1"
@ -51,5 +51,5 @@ tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
tokio = { version = "1.8.4", features = ["sync"] }
[dev-dependencies]
actix-web = { version = "4.0.0-beta.19", default-features = false, features = ["cookies"] }
actix-http = "3.0.0-beta.18"
actix-web = { version = "4.0.0-beta.21", default-features = false, features = ["cookies"] }
actix-http = "3.0.0-beta.19"

View File

@ -3,6 +3,23 @@
## Unreleased - 2021-xx-xx
## 3.0.0-beta.19 - 2022-01-21
### Added
- Response headers can be sent as camel case using `res.head_mut().set_camel_case_headers(true)`. [#2587]
- `ResponseHead` now implements `Clone`. [#2585]
### Changed
- Brotli (de)compression support is now provided by the `brotli` crate. [#2538]
### Removed
- `ResponseHead::extensions[_mut]()`. [#2585]
- `ResponseBuilder::extensions[_mut]()`. [#2585]
[#2538]: https://github.com/actix/actix-web/pull/2538
[#2585]: https://github.com/actix/actix-web/pull/2585
[#2587]: https://github.com/actix/actix-web/pull/2587
## 3.0.0-beta.18 - 2022-01-04
### Added
- `impl Eq` for `header::ContentEncoding`. [#2501]
@ -15,8 +32,8 @@
- `Quality::MIN` is now the smallest non-zero value. [#2501]
- `QualityItem::min` semantics changed with `QualityItem::MIN`. [#2501]
- Rename `ContentEncoding::{Br => Brotli}`. [#2501]
- Minimum supported Rust version (MSRV) is now 1.54.
- Rename `header::EntityTag::{weak => new_weak, strong => new_strong}`. [#2565]
- Minimum supported Rust version (MSRV) is now 1.54.
### Fixed
- `ContentEncoding::Identity` can now be parsed from a string. [#2501]

View File

@ -1,7 +1,10 @@
[package]
name = "actix-http"
version = "3.0.0-beta.18"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
version = "3.0.0-beta.19"
authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>",
]
description = "HTTP primitives for the Actix ecosystem"
keywords = ["actix", "http", "framework", "async", "futures"]
homepage = "https://actix.rs"
@ -33,7 +36,7 @@ openssl = ["actix-tls/accept", "actix-tls/openssl"]
rustls = ["actix-tls/accept", "actix-tls/rustls"]
# enable compression support
compress-brotli = ["brotli2", "__compress"]
compress-brotli = ["brotli", "__compress"]
compress-gzip = ["flate2", "__compress"]
compress-zstd = ["zstd", "__compress"]
@ -74,20 +77,21 @@ smallvec = "1.6.1"
actix-tls = { version = "3.0.0", default-features = false, optional = true }
# compression
brotli2 = { version="0.3.2", optional = true }
brotli = { version = "3.3.3", optional = true }
flate2 = { version = "1.0.13", optional = true }
zstd = { version = "0.9", optional = true }
[dev-dependencies]
actix-http-test = { version = "3.0.0-beta.11", features = ["openssl"] }
actix-server = "2.0.0-rc.2"
actix-server = "2"
actix-tls = { version = "3.0.0", features = ["openssl"] }
actix-web = "4.0.0-beta.19"
actix-web = "4.0.0-beta.21"
async-stream = "0.3"
criterion = { version = "0.3", features = ["html_reports"] }
env_logger = "0.9"
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
memchr = "2.4"
rcgen = "0.8"
regex = "1.3"
rustls-pemfile = "0.2"

View File

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

View File

@ -42,32 +42,37 @@ mod _new {
if x < 10 {
f.write_str("00")?;
// 0 is handled so it's not possible to have a trailing 0, we can just return
itoa::fmt(f, x)
itoa_fmt(f, x)
} else if x < 100 {
f.write_str("0")?;
if x % 10 == 0 {
// trailing 0, divide by 10 and write
itoa::fmt(f, x / 10)
itoa_fmt(f, x / 10)
} else {
itoa::fmt(f, x)
itoa_fmt(f, x)
}
} else {
// x is in range 101999
if x % 100 == 0 {
// two trailing 0s, divide by 100 and write
itoa::fmt(f, x / 100)
itoa_fmt(f, x / 100)
} else if x % 10 == 0 {
// one trailing 0, divide by 10 and write
itoa::fmt(f, x / 10)
itoa_fmt(f, x / 10)
} else {
itoa::fmt(f, x)
itoa_fmt(f, x)
}
}
}
}
}
}
pub fn itoa_fmt<W: fmt::Write, V: itoa::Integer>(mut wr: W, value: V) -> fmt::Result {
let mut buf = itoa::Buffer::new();
wr.write_str(buf.format(value))
}
}
mod _naive {

View File

@ -15,6 +15,7 @@ async fn main() -> io::Result<()> {
HttpService::build()
.client_timeout(1000)
.client_disconnect(1000)
// handles HTTP/1.1 and HTTP/2
.finish(|mut req: Request| async move {
let mut body = BytesMut::new();
while let Some(item) = req.payload().next().await {
@ -23,12 +24,13 @@ async fn main() -> io::Result<()> {
log::info!("request body: {:?}", body);
Ok::<_, Error>(
Response::build(StatusCode::OK)
.insert_header(("x-head", HeaderValue::from_static("dummy value!")))
.body(body),
)
let res = Response::build(StatusCode::OK)
.insert_header(("x-head", HeaderValue::from_static("dummy value!")))
.body(body);
Ok::<_, Error>(res)
})
// No TLS
.tcp()
})?
.run()

View File

@ -1,32 +1,34 @@
use std::io;
use actix_http::{
body::MessageBody, header::HeaderValue, Error, HttpService, Request, Response, StatusCode,
body::{BodyStream, MessageBody},
header, Error, HttpMessage, HttpService, Request, Response, StatusCode,
};
use actix_server::Server;
use bytes::BytesMut;
use futures_util::StreamExt as _;
async fn handle_request(mut req: Request) -> Result<Response<impl MessageBody>, Error> {
let mut body = BytesMut::new();
while let Some(item) = req.payload().next().await {
body.extend_from_slice(&item?)
let mut res = Response::build(StatusCode::OK);
if let Some(ct) = req.headers().get(header::CONTENT_TYPE) {
res.insert_header((header::CONTENT_TYPE, ct));
}
log::info!("request body: {:?}", body);
// echo request payload stream as (chunked) response body
let res = res.message_body(BodyStream::new(req.payload().take()))?;
Ok(Response::build(StatusCode::OK)
.insert_header(("x-head", HeaderValue::from_static("dummy value!")))
.body(body))
Ok(res)
}
#[actix_rt::main]
async fn main() -> io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
Server::build()
actix_server::Server::build()
.bind("echo", ("127.0.0.1", 8080), || {
HttpService::build().finish(handle_request).tcp()
HttpService::build()
// handles HTTP/1.1 only
.h1(handle_request)
// No TLS
.tcp()
})?
.run()
.await

View File

@ -174,12 +174,15 @@ impl ServiceConfig {
}
#[doc(hidden)]
pub fn set_date(&self, dst: &mut BytesMut) {
pub fn set_date(&self, dst: &mut BytesMut, camel_case: bool) {
let mut buf: [u8; 39] = [0; 39];
buf[..6].copy_from_slice(b"date: ");
buf[..6].copy_from_slice(if camel_case { b"Date: " } else { b"date: " });
self.0
.date_service
.set_date(|date| buf[6..35].copy_from_slice(&date.bytes));
buf[35..].copy_from_slice(b"\r\n\r\n");
dst.extend_from_slice(&buf);
}
@ -326,6 +329,7 @@ mod tests {
use super::*;
use actix_rt::{task::yield_now, time::sleep};
use memchr::memmem;
#[actix_rt::test]
async fn test_date_service_update() {
@ -334,7 +338,7 @@ mod tests {
yield_now().await;
let mut buf1 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
settings.set_date(&mut buf1);
settings.set_date(&mut buf1, false);
let now1 = settings.now();
sleep_until(Instant::now() + Duration::from_secs(2)).await;
@ -342,7 +346,7 @@ mod tests {
let now2 = settings.now();
let mut buf2 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
settings.set_date(&mut buf2);
settings.set_date(&mut buf2, false);
assert_ne!(now1, now2);
@ -395,11 +399,27 @@ mod tests {
#[actix_rt::test]
async fn test_date() {
let settings = ServiceConfig::new(KeepAlive::Os, 0, 0, false, None);
let settings = ServiceConfig::default();
let mut buf1 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
settings.set_date(&mut buf1);
settings.set_date(&mut buf1, false);
let mut buf2 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
settings.set_date(&mut buf2);
settings.set_date(&mut buf2, false);
assert_eq!(buf1, buf2);
}
#[actix_rt::test]
async fn test_date_camel_case() {
let settings = ServiceConfig::default();
let mut buf = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
settings.set_date(&mut buf, false);
assert!(memmem::find(&buf, b"date:").is_some());
let mut buf = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
settings.set_date(&mut buf, true);
assert!(memmem::find(&buf, b"Date:").is_some());
}
}

View File

@ -11,9 +11,6 @@ use actix_rt::task::{spawn_blocking, JoinHandle};
use bytes::Bytes;
use futures_core::{ready, Stream};
#[cfg(feature = "compress-brotli")]
use brotli2::write::BrotliDecoder;
#[cfg(feature = "compress-gzip")]
use flate2::write::{GzDecoder, ZlibDecoder};
@ -48,7 +45,7 @@ where
let decoder = match encoding {
#[cfg(feature = "compress-brotli")]
ContentEncoding::Brotli => Some(ContentDecoder::Brotli(Box::new(
BrotliDecoder::new(Writer::new()),
brotli::DecompressorWriter::new(Writer::new(), 8_096),
))),
#[cfg(feature = "compress-gzip")]
ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new(
@ -165,7 +162,7 @@ enum ContentDecoder {
#[cfg(feature = "compress-gzip")]
Gzip(Box<GzDecoder<Writer>>),
#[cfg(feature = "compress-brotli")]
Brotli(Box<BrotliDecoder<Writer>>),
Brotli(Box<brotli::DecompressorWriter<Writer>>),
// We need explicit 'static lifetime here because ZstdDecoder need lifetime
// argument, and we use `spawn_blocking` in `Decoder::poll_next` that require `FnOnce() -> R + Send + 'static`
#[cfg(feature = "compress-zstd")]

View File

@ -14,9 +14,6 @@ use derive_more::Display;
use futures_core::ready;
use pin_project_lite::pin_project;
#[cfg(feature = "compress-brotli")]
use brotli2::write::BrotliEncoder;
#[cfg(feature = "compress-gzip")]
use flate2::write::{GzEncoder, ZlibEncoder};
@ -268,7 +265,7 @@ enum ContentEncoder {
Gzip(GzEncoder<Writer>),
#[cfg(feature = "compress-brotli")]
Brotli(BrotliEncoder<Writer>),
Brotli(Box<brotli::CompressorWriter<Writer>>),
// Wwe need explicit 'static lifetime here because ZstdEncoder needs a lifetime argument and we
// use `spawn_blocking` in `Encoder::poll_next` that requires `FnOnce() -> R + Send + 'static`.
@ -292,9 +289,7 @@ impl ContentEncoder {
))),
#[cfg(feature = "compress-brotli")]
ContentEncoding::Brotli => {
Some(ContentEncoder::Brotli(BrotliEncoder::new(Writer::new(), 3)))
}
ContentEncoding::Brotli => Some(ContentEncoder::Brotli(new_brotli_compressor())),
#[cfg(feature = "compress-zstd")]
ContentEncoding::Zstd => {
@ -326,8 +321,8 @@ impl ContentEncoder {
fn finish(self) -> Result<Bytes, io::Error> {
match self {
#[cfg(feature = "compress-brotli")]
ContentEncoder::Brotli(encoder) => match encoder.finish() {
Ok(writer) => Ok(writer.buf.freeze()),
ContentEncoder::Brotli(mut encoder) => match encoder.flush() {
Ok(()) => Ok(encoder.into_inner().buf.freeze()),
Err(err) => Err(err),
},
@ -392,6 +387,16 @@ impl ContentEncoder {
}
}
#[cfg(feature = "compress-brotli")]
fn new_brotli_compressor() -> Box<brotli::CompressorWriter<Writer>> {
Box::new(brotli::CompressorWriter::new(
Writer::new(),
8 * 1024, // 32 KiB buffer
3, // BROTLI_PARAM_QUALITY
22, // BROTLI_PARAM_LGWIN
))
}
#[derive(Debug, Display)]
#[non_exhaustive]
pub enum EncoderError {

View File

@ -387,6 +387,7 @@ impl StdError for DispatchError {
/// A set of error that can occur during parsing content type.
#[derive(Debug, Display, Error)]
#[cfg_attr(test, derive(PartialEq))]
#[non_exhaustive]
pub enum ContentTypeError {
/// Can not parse content type
@ -398,28 +399,14 @@ pub enum ContentTypeError {
UnknownEncoding,
}
#[cfg(test)]
mod content_type_test_impls {
use super::*;
impl std::cmp::PartialEq for ContentTypeError {
fn eq(&self, other: &Self) -> bool {
match self {
Self::ParseError => matches!(other, ContentTypeError::ParseError),
Self::UnknownEncoding => {
matches!(other, ContentTypeError::UnknownEncoding)
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use http::{Error as HttpError, StatusCode};
use std::io;
use http::{Error as HttpError, StatusCode};
use super::*;
#[test]
fn test_into_response() {
let resp: Response<BoxBody> = ParseError::Incomplete.into();

View File

@ -380,34 +380,36 @@ impl HeaderIndex {
}
#[derive(Debug, Clone, PartialEq)]
/// Http payload item
/// Chunk type yielded while decoding a payload.
pub enum PayloadItem {
Chunk(Bytes),
Eof,
}
/// Decoders to handle different Transfer-Encodings.
/// Decoder that can handle different payload types.
///
/// If a message body does not include a Transfer-Encoding, it *should*
/// include a Content-Length header.
/// If a message body does not use `Transfer-Encoding`, it should include a `Content-Length`.
#[derive(Debug, Clone, PartialEq)]
pub struct PayloadDecoder {
kind: Kind,
}
impl PayloadDecoder {
/// Constructs a fixed-length payload decoder.
pub fn length(x: u64) -> PayloadDecoder {
PayloadDecoder {
kind: Kind::Length(x),
}
}
/// Constructs a chunked encoding decoder.
pub fn chunked() -> PayloadDecoder {
PayloadDecoder {
kind: Kind::Chunked(ChunkedState::Size, 0),
}
}
/// Creates an decoder that yields chunks until the stream returns EOF.
pub fn eof() -> PayloadDecoder {
PayloadDecoder { kind: Kind::Eof }
}
@ -415,25 +417,26 @@ impl PayloadDecoder {
#[derive(Debug, Clone, PartialEq)]
enum Kind {
/// A Reader used when a Content-Length header is passed with a positive
/// integer.
/// A reader used when a `Content-Length` header is passed with a positive integer.
Length(u64),
/// A Reader used when Transfer-Encoding is `chunked`.
/// A reader used when `Transfer-Encoding` is `chunked`.
Chunked(ChunkedState, u64),
/// A Reader used for responses that don't indicate a length or chunked.
/// A reader used for responses that don't indicate a length or chunked.
///
/// Note: This should only used for `Response`s. It is illegal for a
/// `Request` to be made with both `Content-Length` and
/// `Transfer-Encoding: chunked` missing, as explained from the spec:
/// Note: This should only used for `Response`s. It is illegal for a `Request` to be made
/// without either of `Content-Length` and `Transfer-Encoding: chunked` missing, as explained
/// in [RFC 7230 §3.3.3]:
///
/// > If a Transfer-Encoding header field is present in a response and
/// > the chunked transfer coding is not the final encoding, the
/// > message body length is determined by reading the connection until
/// > it is closed by the server. If a Transfer-Encoding header field
/// > is present in a request and the chunked transfer coding is not
/// > the final encoding, the message body length cannot be determined
/// > reliably; the server MUST respond with the 400 (Bad Request)
/// > status code and then close the connection.
/// > If a Transfer-Encoding header field is present in a response and the chunked transfer
/// > coding is not the final encoding, the message body length is determined by reading the
/// > connection until it is closed by the server. If a Transfer-Encoding header field is
/// > present in a request and the chunked transfer coding is not the final encoding, the
/// > message body length cannot be determined reliably; the server MUST respond with the 400
/// > (Bad Request) status code and then close the connection.
///
/// [RFC 7230 §3.3.3]: https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3
Eof,
}
@ -463,6 +466,7 @@ impl Decoder for PayloadDecoder {
Ok(Some(PayloadItem::Chunk(buf)))
}
}
Kind::Chunked(ref mut state, ref mut size) => {
loop {
let mut buf = None;
@ -488,6 +492,7 @@ impl Decoder for PayloadDecoder {
}
}
}
Kind::Eof => {
if src.is_empty() {
Ok(None)

View File

@ -105,7 +105,7 @@ pub(crate) trait MessageType: Sized {
}
BodySize::Sized(0) if camel_case => dst.put_slice(b"\r\nContent-Length: 0\r\n"),
BodySize::Sized(0) => dst.put_slice(b"\r\ncontent-length: 0\r\n"),
BodySize::Sized(len) => helpers::write_content_length(len, dst),
BodySize::Sized(len) => helpers::write_content_length(len, dst, camel_case),
BodySize::None => dst.put_slice(b"\r\n"),
}
@ -213,7 +213,7 @@ pub(crate) trait MessageType: Sized {
// optimized date header, set_date writes \r\n
if !has_date {
config.set_date(dst);
config.set_date(dst, camel_case);
} else {
// msg eof
dst.extend_from_slice(b"\r\n");
@ -258,6 +258,12 @@ impl MessageType for Response<()> {
None
}
fn camel_case(&self) -> bool {
self.head()
.flags
.contains(crate::message::Flags::CAMEL_CASE)
}
fn encode_status(&mut self, dst: &mut BytesMut) -> io::Result<()> {
let head = self.head();
let reason = head.reason().as_bytes();

View File

@ -30,15 +30,25 @@ pub(crate) fn write_status_line<B: BufMut>(version: Version, n: u16, buf: &mut B
/// Write out content length header.
///
/// Buffer must to contain enough space or be implicitly extendable.
pub fn write_content_length<B: BufMut>(n: u64, buf: &mut B) {
pub fn write_content_length<B: BufMut>(n: u64, buf: &mut B, camel_case: bool) {
if n == 0 {
buf.put_slice(b"\r\ncontent-length: 0\r\n");
if camel_case {
buf.put_slice(b"\r\nContent-Length: 0\r\n");
} else {
buf.put_slice(b"\r\ncontent-length: 0\r\n");
}
return;
}
let mut buffer = itoa::Buffer::new();
buf.put_slice(b"\r\ncontent-length: ");
if camel_case {
buf.put_slice(b"\r\nContent-Length: ");
} else {
buf.put_slice(b"\r\ncontent-length: ");
}
buf.put_slice(buffer.format(n).as_bytes());
buf.put_slice(b"\r\n");
}
@ -95,77 +105,88 @@ mod tests {
fn test_write_content_length() {
let mut bytes = BytesMut::new();
bytes.reserve(50);
write_content_length(0, &mut bytes);
write_content_length(0, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 0\r\n"[..]);
bytes.reserve(50);
write_content_length(9, &mut bytes);
write_content_length(9, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 9\r\n"[..]);
bytes.reserve(50);
write_content_length(10, &mut bytes);
write_content_length(10, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 10\r\n"[..]);
bytes.reserve(50);
write_content_length(99, &mut bytes);
write_content_length(99, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 99\r\n"[..]);
bytes.reserve(50);
write_content_length(100, &mut bytes);
write_content_length(100, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 100\r\n"[..]);
bytes.reserve(50);
write_content_length(101, &mut bytes);
write_content_length(101, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 101\r\n"[..]);
bytes.reserve(50);
write_content_length(998, &mut bytes);
write_content_length(998, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 998\r\n"[..]);
bytes.reserve(50);
write_content_length(1000, &mut bytes);
write_content_length(1000, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 1000\r\n"[..]);
bytes.reserve(50);
write_content_length(1001, &mut bytes);
write_content_length(1001, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 1001\r\n"[..]);
bytes.reserve(50);
write_content_length(5909, &mut bytes);
write_content_length(5909, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 5909\r\n"[..]);
bytes.reserve(50);
write_content_length(9999, &mut bytes);
write_content_length(9999, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 9999\r\n"[..]);
bytes.reserve(50);
write_content_length(10001, &mut bytes);
write_content_length(10001, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 10001\r\n"[..]);
bytes.reserve(50);
write_content_length(59094, &mut bytes);
write_content_length(59094, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 59094\r\n"[..]);
bytes.reserve(50);
write_content_length(99999, &mut bytes);
write_content_length(99999, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 99999\r\n"[..]);
bytes.reserve(50);
write_content_length(590947, &mut bytes);
write_content_length(590947, &mut bytes, false);
assert_eq!(
bytes.split().freeze(),
b"\r\ncontent-length: 590947\r\n"[..]
);
bytes.reserve(50);
write_content_length(999999, &mut bytes);
write_content_length(999999, &mut bytes, false);
assert_eq!(
bytes.split().freeze(),
b"\r\ncontent-length: 999999\r\n"[..]
);
bytes.reserve(50);
write_content_length(5909471, &mut bytes);
write_content_length(5909471, &mut bytes, false);
assert_eq!(
bytes.split().freeze(),
b"\r\ncontent-length: 5909471\r\n"[..]
);
bytes.reserve(50);
write_content_length(59094718, &mut bytes);
write_content_length(59094718, &mut bytes, false);
assert_eq!(
bytes.split().freeze(),
b"\r\ncontent-length: 59094718\r\n"[..]
);
bytes.reserve(50);
write_content_length(4294973728, &mut bytes);
write_content_length(4294973728, &mut bytes, false);
assert_eq!(
bytes.split().freeze(),
b"\r\ncontent-length: 4294973728\r\n"[..]
);
}
#[test]
fn write_content_length_camel_case() {
let mut bytes = BytesMut::new();
write_content_length(0, &mut bytes, false);
assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 0\r\n"[..]);
let mut bytes = BytesMut::new();
write_content_length(0, &mut bytes, true);
assert_eq!(bytes.split().freeze(), b"\r\nContent-Length: 0\r\n"[..]);
}
}

View File

@ -25,10 +25,10 @@ pub trait HttpMessage: Sized {
/// Message payload stream
fn take_payload(&mut self) -> Payload<Self::Stream>;
/// Request's extensions container
/// Returns a reference to the request-local data/extensions container.
fn extensions(&self) -> Ref<'_, Extensions>;
/// Mutable reference to a the request's extensions container
/// Returns a mutable reference to the request-local data/extensions container.
fn extensions_mut(&self) -> RefMut<'_, Extensions>;
/// Get a header.

View File

@ -5,13 +5,13 @@ use bitflags::bitflags;
/// Represents various types of connection
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum ConnectionType {
/// Close connection after response
/// Close connection after response.
Close,
/// Keep connection alive after response
/// Keep connection alive after response.
KeepAlive,
/// Connection is upgraded to different type
/// Connection is upgraded to different type.
Upgrade,
}
@ -69,8 +69,8 @@ impl<T: Head> Drop for Message<T> {
}
}
/// Generic `Head` object pool.
#[doc(hidden)]
/// Request's objects pool
pub struct MessagePool<T: Head>(RefCell<Vec<Rc<T>>>);
impl<T: Head> MessagePool<T> {

View File

@ -142,8 +142,8 @@ impl RequestHead {
}
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum RequestHeadType {
Owned(RequestHead),
Rc(Rc<RequestHead>, Option<HeaderMap>),

View File

@ -19,7 +19,7 @@ pub struct Request<P = BoxedPayloadStream> {
pub(crate) payload: Payload<P>,
pub(crate) head: Message<RequestHead>,
pub(crate) conn_data: Option<Rc<Extensions>>,
pub(crate) req_data: RefCell<Extensions>,
pub(crate) extensions: RefCell<Extensions>,
}
impl<P> HttpMessage for Request<P> {
@ -34,16 +34,14 @@ impl<P> HttpMessage for Request<P> {
mem::replace(&mut self.payload, Payload::None)
}
/// Request extensions
#[inline]
fn extensions(&self) -> Ref<'_, Extensions> {
self.req_data.borrow()
self.extensions.borrow()
}
/// Mutable reference to a the request's extensions
#[inline]
fn extensions_mut(&self) -> RefMut<'_, Extensions> {
self.req_data.borrow_mut()
self.extensions.borrow_mut()
}
}
@ -52,7 +50,7 @@ impl From<Message<RequestHead>> for Request<BoxedPayloadStream> {
Request {
head,
payload: Payload::None,
req_data: RefCell::new(Extensions::default()),
extensions: RefCell::new(Extensions::default()),
conn_data: None,
}
}
@ -65,7 +63,7 @@ impl Request<BoxedPayloadStream> {
Request {
head: Message::new(),
payload: Payload::None,
req_data: RefCell::new(Extensions::default()),
extensions: RefCell::new(Extensions::default()),
conn_data: None,
}
}
@ -77,7 +75,7 @@ impl<P> Request<P> {
Request {
payload,
head: Message::new(),
req_data: RefCell::new(Extensions::default()),
extensions: RefCell::new(Extensions::default()),
conn_data: None,
}
}
@ -90,7 +88,7 @@ impl<P> Request<P> {
Request {
payload,
head: self.head,
req_data: self.req_data,
extensions: self.extensions,
conn_data: self.conn_data,
},
pl,
@ -195,16 +193,17 @@ impl<P> Request<P> {
.and_then(|container| container.get::<T>())
}
/// Returns the connection data container if an [on-connect] callback was registered.
/// Returns the connection-level data/extensions container if an [on-connect] callback was
/// registered, leaving an empty one in its place.
///
/// [on-connect]: crate::HttpServiceBuilder::on_connect_ext
pub fn take_conn_data(&mut self) -> Option<Rc<Extensions>> {
self.conn_data.take()
}
/// Returns the request data container, leaving an empty one in it's place.
/// Returns the request-local data/extensions container, leaving an empty one in its place.
pub fn take_req_data(&mut self) -> Extensions {
mem::take(self.req_data.get_mut())
mem::take(self.extensions.get_mut())
}
}

View File

@ -1,9 +1,6 @@
//! HTTP response builder.
use std::{
cell::{Ref, RefMut},
fmt, str,
};
use std::{cell::RefCell, fmt, str};
use crate::{
body::{EitherBody, MessageBody},
@ -202,20 +199,6 @@ impl ResponseBuilder {
self
}
/// Responses extensions
#[inline]
pub fn extensions(&self) -> Ref<'_, Extensions> {
let head = self.head.as_ref().expect("cannot reuse response builder");
head.extensions.borrow()
}
/// Mutable reference to a the response's extensions
#[inline]
pub fn extensions_mut(&mut self) -> RefMut<'_, Extensions> {
let head = self.head.as_ref().expect("cannot reuse response builder");
head.extensions.borrow_mut()
}
/// Generate response with a wrapped body.
///
/// This `ResponseBuilder` will be left in a useless state.
@ -238,7 +221,12 @@ impl ResponseBuilder {
}
let head = self.head.take().expect("cannot reuse response builder");
Ok(Response { head, body })
Ok(Response {
head,
body,
extensions: RefCell::new(Extensions::new()),
})
}
/// Generate response with an empty body.

View File

@ -1,26 +1,20 @@
//! Response head type and caching pool.
use std::{
cell::{Ref, RefCell, RefMut},
ops,
};
use std::{cell::RefCell, ops};
use crate::{
header::HeaderMap, message::Flags, ConnectionType, Extensions, StatusCode, Version,
};
use crate::{header::HeaderMap, message::Flags, ConnectionType, StatusCode, Version};
thread_local! {
static RESPONSE_POOL: BoxedResponsePool = BoxedResponsePool::create();
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct ResponseHead {
pub version: Version,
pub status: StatusCode,
pub headers: HeaderMap,
pub reason: Option<&'static str>,
pub(crate) extensions: RefCell<Extensions>,
flags: Flags,
pub(crate) flags: Flags,
}
impl ResponseHead {
@ -33,36 +27,35 @@ impl ResponseHead {
headers: HeaderMap::with_capacity(12),
reason: None,
flags: Flags::empty(),
extensions: RefCell::new(Extensions::new()),
}
}
#[inline]
/// Read the message headers.
#[inline]
pub fn headers(&self) -> &HeaderMap {
&self.headers
}
#[inline]
/// Mutable reference to the message headers.
#[inline]
pub fn headers_mut(&mut self) -> &mut HeaderMap {
&mut self.headers
}
/// Message extensions
/// Sets the flag that controls wether to send headers formatted as Camel-Case.
///
/// Only applicable to HTTP/1.x responses; HTTP/2 header names are always lowercase.
#[inline]
pub fn extensions(&self) -> Ref<'_, Extensions> {
self.extensions.borrow()
pub fn set_camel_case_headers(&mut self, camel_case: bool) {
if camel_case {
self.flags.insert(Flags::CAMEL_CASE);
} else {
self.flags.remove(Flags::CAMEL_CASE);
}
}
/// Mutable reference to a the message's extensions
#[inline]
pub fn extensions_mut(&self) -> RefMut<'_, Extensions> {
self.extensions.borrow_mut()
}
#[inline]
/// Set connection type of the message
#[inline]
pub fn set_connection_type(&mut self, ctype: ConnectionType) {
match ctype {
ConnectionType::Close => self.flags.insert(Flags::CLOSE),
@ -121,14 +114,14 @@ impl ResponseHead {
}
}
#[inline]
/// Get response body chunking state
#[inline]
pub fn chunked(&self) -> bool {
!self.flags.contains(Flags::NO_CHUNKING)
}
#[inline]
/// Set no chunking for payload
#[inline]
pub fn no_chunking(&mut self, val: bool) {
if val {
self.flags.insert(Flags::NO_CHUNKING);
@ -171,7 +164,7 @@ impl Drop for BoxedResponseHead {
}
}
/// Request's objects pool
/// Response head object pool.
#[doc(hidden)]
pub struct BoxedResponsePool(#[allow(clippy::vec_box)] RefCell<Vec<Box<ResponseHead>>>);
@ -180,7 +173,7 @@ impl BoxedResponsePool {
BoxedResponsePool(RefCell::new(Vec::with_capacity(128)))
}
/// Get message from the pool
/// Get message from the pool.
#[inline]
fn get_message(&self, status: StatusCode) -> BoxedResponseHead {
if let Some(mut head) = self.0.borrow_mut().pop() {
@ -196,13 +189,75 @@ impl BoxedResponsePool {
}
}
/// Release request instance
/// Release request instance.
#[inline]
fn release(&self, mut msg: Box<ResponseHead>) {
fn release(&self, msg: Box<ResponseHead>) {
let pool = &mut self.0.borrow_mut();
if pool.len() < 128 {
msg.extensions.get_mut().clear();
pool.push(msg);
}
}
}
#[cfg(test)]
mod tests {
use std::{
io::{Read as _, Write as _},
net,
};
use memchr::memmem;
use crate::{
header::{HeaderName, HeaderValue},
Error, HttpService, Request, Response,
};
#[actix_rt::test]
async fn camel_case_headers() {
let mut srv = actix_http_test::test_server(|| {
HttpService::new(|req: Request| async move {
let mut res = Response::ok();
if req.path().contains("camel") {
res.head_mut().set_camel_case_headers(true);
}
res.headers_mut().insert(
HeaderName::from_static("foo-bar"),
HeaderValue::from_static("baz"),
);
Ok::<_, Error>(res)
})
.tcp()
})
.await;
let mut stream = net::TcpStream::connect(srv.addr()).unwrap();
let _ = stream.write_all(b"GET /camel HTTP/1.1\r\nConnection: Close\r\n\r\n");
let mut data = vec![0; 1024];
let _ = stream.read(&mut data);
assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n");
assert!(memmem::find(&data, b"Foo-Bar").is_some());
assert!(memmem::find(&data, b"foo-bar").is_none());
assert!(memmem::find(&data, b"Date").is_some());
assert!(memmem::find(&data, b"date").is_none());
assert!(memmem::find(&data, b"Content-Length").is_some());
assert!(memmem::find(&data, b"content-length").is_none());
let mut stream = net::TcpStream::connect(srv.addr()).unwrap();
let _ = stream.write_all(b"GET /lower HTTP/1.1\r\nConnection: Close\r\n\r\n");
let mut data = vec![0; 1024];
let _ = stream.read(&mut data);
assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n");
assert!(memmem::find(&data, b"Foo-Bar").is_none());
assert!(memmem::find(&data, b"foo-bar").is_some());
assert!(memmem::find(&data, b"Date").is_none());
assert!(memmem::find(&data, b"date").is_some());
assert!(memmem::find(&data, b"Content-Length").is_none());
assert!(memmem::find(&data, b"content-length").is_some());
srv.stop().await;
}
}

View File

@ -1,7 +1,7 @@
//! HTTP response.
use std::{
cell::{Ref, RefMut},
cell::{Ref, RefCell, RefMut},
fmt, str,
};
@ -9,7 +9,7 @@ use bytes::{Bytes, BytesMut};
use bytestring::ByteString;
use crate::{
body::{BoxBody, MessageBody},
body::{BoxBody, EitherBody, MessageBody},
header::{self, HeaderMap, TryIntoHeaderValue},
responses::BoxedResponseHead,
Error, Extensions, ResponseBuilder, ResponseHead, StatusCode,
@ -19,6 +19,7 @@ use crate::{
pub struct Response<B> {
pub(crate) head: BoxedResponseHead,
pub(crate) body: B,
pub(crate) extensions: RefCell<Extensions>,
}
impl Response<BoxBody> {
@ -28,6 +29,7 @@ impl Response<BoxBody> {
Response {
head: BoxedResponseHead::new(status),
body: BoxBody::new(()),
extensions: RefCell::new(Extensions::new()),
}
}
@ -74,6 +76,7 @@ impl<B> Response<B> {
Response {
head: BoxedResponseHead::new(status),
body,
extensions: RefCell::new(Extensions::new()),
}
}
@ -120,20 +123,21 @@ impl<B> Response<B> {
}
/// Returns true if keep-alive is enabled.
#[inline]
pub fn keep_alive(&self) -> bool {
self.head.keep_alive()
}
/// Returns a reference to the extensions of this response.
/// Returns a reference to the request-local data/extensions container.
#[inline]
pub fn extensions(&self) -> Ref<'_, Extensions> {
self.head.extensions.borrow()
self.extensions.borrow()
}
/// Returns a mutable reference to the extensions of this response.
/// Returns a mutable reference to the request-local data/extensions container.
#[inline]
pub fn extensions_mut(&mut self) -> RefMut<'_, Extensions> {
self.head.extensions.borrow_mut()
self.extensions.borrow_mut()
}
/// Returns a reference to the body of this response.
@ -143,24 +147,29 @@ impl<B> Response<B> {
}
/// Sets new body.
#[inline]
pub fn set_body<B2>(self, body: B2) -> Response<B2> {
Response {
head: self.head,
body,
extensions: self.extensions,
}
}
/// Drops body and returns new response.
#[inline]
pub fn drop_body(self) -> Response<()> {
self.set_body(())
}
/// Sets new body, returning new response and previous body value.
#[inline]
pub(crate) fn replace_body<B2>(self, body: B2) -> (Response<B2>, B) {
(
Response {
head: self.head,
body,
extensions: self.extensions,
},
self.body,
)
@ -171,11 +180,15 @@ impl<B> Response<B> {
/// # Implementation Notes
/// Due to internal performance optimizations, the first element of the returned tuple is a
/// `Response` as well but only contains the head of the response this was called on.
#[inline]
pub fn into_parts(self) -> (Response<()>, B) {
self.replace_body(())
}
/// Returns new response with mapped body.
/// Map the current body type to another using a closure. Returns a new response.
///
/// Closure receives the response head and the current body type.
#[inline]
pub fn map_body<F, B2>(mut self, f: F) -> Response<B2>
where
F: FnOnce(&mut ResponseHead, B) -> B2,
@ -185,6 +198,7 @@ impl<B> Response<B> {
Response {
head: self.head,
body,
extensions: self.extensions,
}
}
@ -197,6 +211,7 @@ impl<B> Response<B> {
}
/// Returns body, consuming this response.
#[inline]
pub fn into_body(self) -> B {
self.body
}
@ -239,9 +254,9 @@ impl<I: Into<Response<BoxBody>>, E: Into<Error>> From<Result<I, E>> for Response
}
}
impl From<ResponseBuilder> for Response<BoxBody> {
impl From<ResponseBuilder> for Response<EitherBody<()>> {
fn from(mut builder: ResponseBuilder) -> Self {
builder.finish().map_into_boxed_body()
builder.finish()
}
}

View File

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

View File

@ -3,6 +3,20 @@
## Unreleased - 2021-xx-xx
## 0.5.0-rc.2 - 2022-01-21
- Add `Path::as_str`. [#2590]
- Deprecate `Path::path`. [#2590]
[#2590]: https://github.com/actix/actix-web/pull/2590
## 0.5.0-rc.1 - 2022-01-14
- `Resource` trait now have an associated type, `Path`, instead of the generic parameter. [#2568]
- `Resource` is now implemented for `&mut Path<_>` and `RefMut<Path<_>>`. [#2568]
[#2568]: https://github.com/actix/actix-web/pull/2568
## 0.5.0-beta.4 - 2022-01-04
- `PathDeserializer` now decodes all percent encoded characters in dynamic segments. [#2566]
- Minimum supported Rust version (MSRV) is now 1.54.

View File

@ -1,6 +1,6 @@
[package]
name = "actix-router"
version = "0.5.0-beta.4"
version = "0.5.0-rc.2"
authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>",

View File

@ -1,3 +1,5 @@
use std::borrow::Cow;
use serde::de::{self, Deserializer, Error as DeError, Visitor};
use serde::forward_to_deserialize_any;
@ -20,7 +22,7 @@ macro_rules! unsupported_type {
}
macro_rules! parse_single_value {
($trait_fn:ident, $visit_fn:ident, $tp:expr) => {
($trait_fn:ident) => {
fn $trait_fn<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
@ -34,15 +36,10 @@ macro_rules! parse_single_value {
.as_str(),
))
} else {
let decoded = FULL_QUOTER
.with(|q| q.requote(self.path[0].as_bytes()))
.unwrap_or_else(|| self.path[0].to_owned());
let v = decoded.parse().map_err(|_| {
de::Error::custom(format!("can not parse {:?} to a {}", &self.path[0], $tp))
})?;
visitor.$visit_fn(v)
Value {
value: &self.path[0],
}
.$trait_fn(visitor)
}
}
};
@ -56,7 +53,8 @@ macro_rules! parse_value {
{
let decoded = FULL_QUOTER
.with(|q| q.requote(self.value.as_bytes()))
.unwrap_or_else(|| self.value.to_owned());
.map(Cow::Owned)
.unwrap_or(Cow::Borrowed(self.value));
let v = decoded.parse().map_err(|_| {
de::value::Error::custom(format!("can not parse {:?} to a {}", self.value, $tp))
@ -204,26 +202,26 @@ impl<'de, T: ResourcePath + 'de> Deserializer<'de> for PathDeserializer<'de, T>
}
unsupported_type!(deserialize_any, "'any'");
unsupported_type!(deserialize_bytes, "bytes");
unsupported_type!(deserialize_option, "Option<T>");
unsupported_type!(deserialize_identifier, "identifier");
unsupported_type!(deserialize_ignored_any, "ignored_any");
parse_single_value!(deserialize_bool, visit_bool, "bool");
parse_single_value!(deserialize_i8, visit_i8, "i8");
parse_single_value!(deserialize_i16, visit_i16, "i16");
parse_single_value!(deserialize_i32, visit_i32, "i32");
parse_single_value!(deserialize_i64, visit_i64, "i64");
parse_single_value!(deserialize_u8, visit_u8, "u8");
parse_single_value!(deserialize_u16, visit_u16, "u16");
parse_single_value!(deserialize_u32, visit_u32, "u32");
parse_single_value!(deserialize_u64, visit_u64, "u64");
parse_single_value!(deserialize_f32, visit_f32, "f32");
parse_single_value!(deserialize_f64, visit_f64, "f64");
parse_single_value!(deserialize_str, visit_string, "String");
parse_single_value!(deserialize_string, visit_string, "String");
parse_single_value!(deserialize_byte_buf, visit_string, "String");
parse_single_value!(deserialize_char, visit_char, "char");
parse_single_value!(deserialize_bool);
parse_single_value!(deserialize_i8);
parse_single_value!(deserialize_i16);
parse_single_value!(deserialize_i32);
parse_single_value!(deserialize_i64);
parse_single_value!(deserialize_u8);
parse_single_value!(deserialize_u16);
parse_single_value!(deserialize_u32);
parse_single_value!(deserialize_u64);
parse_single_value!(deserialize_f32);
parse_single_value!(deserialize_f64);
parse_single_value!(deserialize_str);
parse_single_value!(deserialize_string);
parse_single_value!(deserialize_bytes);
parse_single_value!(deserialize_byte_buf);
parse_single_value!(deserialize_char);
}
struct ParamsDeserializer<'de, T: ResourcePath> {
@ -303,8 +301,6 @@ impl<'de> Deserializer<'de> for Value<'de> {
parse_value!(deserialize_u64, visit_u64, "u64");
parse_value!(deserialize_f32, visit_f32, "f32");
parse_value!(deserialize_f64, visit_f64, "f64");
parse_value!(deserialize_string, visit_string, "String");
parse_value!(deserialize_byte_buf, visit_string, "String");
parse_value!(deserialize_char, visit_char, "char");
fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
@ -332,18 +328,38 @@ impl<'de> Deserializer<'de> for Value<'de> {
visitor.visit_unit()
}
fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_borrowed_bytes(self.value.as_bytes())
}
fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_borrowed_str(self.value)
match FULL_QUOTER.with(|q| q.requote(self.value.as_bytes())) {
Some(s) => visitor.visit_string(s),
None => visitor.visit_borrowed_str(self.value),
}
}
fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
match FULL_QUOTER.with(|q| q.requote(self.value.as_bytes())) {
Some(s) => visitor.visit_byte_buf(s.into()),
None => visitor.visit_borrowed_bytes(self.value.as_bytes()),
}
}
fn deserialize_byte_buf<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
self.deserialize_bytes(visitor)
}
fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
self.deserialize_str(visitor)
}
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
@ -671,12 +687,12 @@ mod tests {
fn deserialize_path_decode_seq() {
let rdef = ResourceDef::new("/{key}/{value}");
let mut path = Path::new("/%25/%2F");
let mut path = Path::new("/%30%25/%30%2F");
rdef.capture_match_info(&mut path);
let de = PathDeserializer::new(&path);
let segment: (String, String) = serde::Deserialize::deserialize(de).unwrap();
assert_eq!(segment.0, "%");
assert_eq!(segment.1, "/");
assert_eq!(segment.0, "0%");
assert_eq!(segment.1, "0/");
}
#[test]
@ -697,6 +713,32 @@ mod tests {
assert_eq!(vals.value, "/");
}
#[test]
fn deserialize_borrowed() {
#[derive(Debug, Deserialize)]
struct Params<'a> {
val: &'a str,
}
let rdef = ResourceDef::new("/{val}");
let mut path = Path::new("/X");
rdef.capture_match_info(&mut path);
let de = PathDeserializer::new(&path);
let params: Params<'_> = serde::Deserialize::deserialize(de).unwrap();
assert_eq!(params.val, "X");
let de = PathDeserializer::new(&path);
let params: &str = serde::Deserialize::deserialize(de).unwrap();
assert_eq!(params, "X");
let mut path = Path::new("/%2F");
rdef.capture_match_info(&mut path);
let de = PathDeserializer::new(&path);
assert!(<Params<'_> as serde::Deserialize>::deserialize(de).is_err());
let de = PathDeserializer::new(&path);
assert!(<&str as serde::Deserialize>::deserialize(de).is_err());
}
// #[test]
// fn test_extract_path_decode() {
// let mut router = Router::<()>::default();

View File

@ -1,5 +1,5 @@
use std::borrow::Cow;
use std::ops::Index;
use std::ops::{DerefMut, Index};
use firestorm::profile_method;
use serde::de;
@ -37,19 +37,39 @@ impl<T: ResourcePath> Path<T> {
}
}
/// Get reference to inner path instance.
/// Returns reference to inner path instance.
#[inline]
pub fn get_ref(&self) -> &T {
&self.path
}
/// Get mutable reference to inner path instance.
/// Returns mutable reference to inner path instance.
#[inline]
pub fn get_mut(&mut self) -> &mut T {
&mut self.path
}
/// Path.
/// Returns full path as a string.
#[inline]
pub fn as_str(&self) -> &str {
profile_method!(as_str);
self.path.path()
}
/// Returns unprocessed part of the path.
///
/// Returns empty string if no more is to be processed.
#[inline]
pub fn unprocessed(&self) -> &str {
profile_method!(unprocessed);
// clamp skip to path length
let skip = (self.skip as usize).min(self.as_str().len());
&self.path.path()[skip..]
}
/// Returns unprocessed part of the path.
#[doc(hidden)]
#[deprecated(since = "0.6.0", note = "Use `.as_str()` or `.unprocessed()`.")]
#[inline]
pub fn path(&self) -> &str {
profile_method!(path);
@ -66,6 +86,8 @@ impl<T: ResourcePath> Path<T> {
/// Set new path.
#[inline]
pub fn set(&mut self, path: T) {
profile_method!(set);
self.skip = 0;
self.path = path;
self.segments.clear();
@ -74,6 +96,8 @@ impl<T: ResourcePath> Path<T> {
/// Reset state.
#[inline]
pub fn reset(&mut self) {
profile_method!(reset);
self.skip = 0;
self.segments.clear();
}
@ -81,6 +105,7 @@ impl<T: ResourcePath> Path<T> {
/// Skip first `n` chars in path.
#[inline]
pub fn skip(&mut self, n: u16) {
profile_method!(skip);
self.skip += n;
}
@ -102,6 +127,8 @@ impl<T: ResourcePath> Path<T> {
name: impl Into<Cow<'static, str>>,
value: impl Into<Cow<'static, str>>,
) {
profile_method!(add_static);
self.segments
.push((name.into(), PathItem::Static(value.into())));
}
@ -136,11 +163,6 @@ impl<T: ResourcePath> Path<T> {
None
}
/// Get unprocessed part of the path
pub fn unprocessed(&self) -> &str {
&self.path.path()[(self.skip as usize)..]
}
/// Get matched parameter by name.
///
/// If keyed parameter is not available empty string is used as default value.
@ -213,8 +235,38 @@ impl<T: ResourcePath> Index<usize> for Path<T> {
}
}
impl<T: ResourcePath> Resource<T> for Path<T> {
fn resource_path(&mut self) -> &mut Self {
impl<T: ResourcePath> Resource for Path<T> {
type Path = T;
fn resource_path(&mut self) -> &mut Path<Self::Path> {
self
}
}
impl<T, P> Resource for T
where
T: DerefMut<Target = Path<P>>,
P: ResourcePath,
{
type Path = P;
fn resource_path(&mut self) -> &mut Path<Self::Path> {
&mut *self
}
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use super::*;
#[test]
fn deref_impls() {
let mut foo = Path::new("/foo");
let _ = (&mut foo).resource_path();
let foo = RefCell::new(foo);
let _ = foo.borrow_mut().resource_path();
}
}

View File

@ -678,22 +678,21 @@ impl ResourceDef {
/// assert!(!try_match(&resource, &mut path));
/// assert_eq!(path.unprocessed(), "/user/admin/stars");
/// ```
pub fn capture_match_info_fn<R, T, F, U>(
pub fn capture_match_info_fn<R, F, U>(
&self,
resource: &mut R,
check_fn: F,
user_data: U,
) -> bool
where
R: Resource<T>,
T: ResourcePath,
R: Resource,
F: FnOnce(&R, U) -> bool,
{
profile_method!(capture_match_info_fn);
let mut segments = <[PathItem; MAX_DYNAMIC_SEGMENTS]>::default();
let path = resource.resource_path();
let path_str = path.path();
let path_str = path.unprocessed();
let (matched_len, matched_vars) = match &self.pat_type {
PatternType::Static(pattern) => {
@ -711,7 +710,7 @@ impl ResourceDef {
let captures = {
profile_section!(pattern_dynamic_regex_exec);
match re.captures(path.path()) {
match re.captures(path.unprocessed()) {
Some(captures) => captures,
_ => return false,
}
@ -739,7 +738,7 @@ impl ResourceDef {
PatternType::DynamicSet(re, params) => {
profile_section!(pattern_dynamic_set);
let path = path.path();
let path = path.unprocessed();
let (pattern, names) = match re.matches(path).into_iter().next() {
Some(idx) => &params[idx],
_ => return false,

View File

@ -2,8 +2,11 @@ use crate::Path;
// TODO: this trait is necessary, document it
// see impl Resource for ServiceRequest
pub trait Resource<T: ResourcePath> {
fn resource_path(&mut self) -> &mut Path<T>;
pub trait Resource {
/// Type of resource's path returned in `resource_path`.
type Path: ResourcePath;
fn resource_path(&mut self) -> &mut Path<Self::Path>;
}
pub trait ResourcePath {

View File

@ -1,6 +1,6 @@
use firestorm::profile_method;
use crate::{IntoPatterns, Resource, ResourceDef, ResourcePath};
use crate::{IntoPatterns, Resource, ResourceDef};
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct ResourceId(pub u16);
@ -26,10 +26,9 @@ impl<T, U> Router<T, U> {
}
}
pub fn recognize<R, P>(&self, resource: &mut R) -> Option<(&T, ResourceId)>
pub fn recognize<R>(&self, resource: &mut R) -> Option<(&T, ResourceId)>
where
R: Resource<P>,
P: ResourcePath,
R: Resource,
{
profile_method!(recognize);
@ -42,10 +41,9 @@ impl<T, U> Router<T, U> {
None
}
pub fn recognize_mut<R, P>(&mut self, resource: &mut R) -> Option<(&mut T, ResourceId)>
pub fn recognize_mut<R>(&mut self, resource: &mut R) -> Option<(&mut T, ResourceId)>
where
R: Resource<P>,
P: ResourcePath,
R: Resource,
{
profile_method!(recognize_mut);
@ -58,11 +56,10 @@ impl<T, U> Router<T, U> {
None
}
pub fn recognize_fn<R, P, F>(&self, resource: &mut R, check: F) -> Option<(&T, ResourceId)>
pub fn recognize_fn<R, F>(&self, resource: &mut R, check: F) -> Option<(&T, ResourceId)>
where
F: Fn(&R, &Option<U>) -> bool,
R: Resource<P>,
P: ResourcePath,
R: Resource,
{
profile_method!(recognize_checked);
@ -75,15 +72,14 @@ impl<T, U> Router<T, U> {
None
}
pub fn recognize_mut_fn<R, P, F>(
pub fn recognize_mut_fn<R, F>(
&mut self,
resource: &mut R,
check: F,
) -> Option<(&mut T, ResourceId)>
where
F: Fn(&R, &Option<U>) -> bool,
R: Resource<P>,
P: ResourcePath,
R: Resource,
{
profile_method!(recognize_mut_checked);
@ -260,6 +256,7 @@ mod tests {
router.path("/name/{val}", 11);
let mut router = router.finish();
// test skip beyond path length
let mut path = Path::new("/name");
path.skip(6);
assert!(router.recognize_mut(&mut path).is_none());

View File

@ -121,7 +121,7 @@ mod tests {
}
#[test]
fn valid_utf8_multibyte() {
fn valid_utf8_multi_byte() {
let test = ('\u{FF00}'..='\u{FFFF}').collect::<String>();
let encoded = percent_encode(test.as_bytes());
let path = match_url("/a/{id}/b", format!("/a/{}/b", &encoded));
@ -135,6 +135,6 @@ mod tests {
let path = Path::new(Url::new(uri));
// We should always get a valid utf8 string
assert!(String::from_utf8(path.path().as_bytes().to_owned()).is_ok());
assert!(String::from_utf8(path.as_str().as_bytes().to_owned()).is_ok());
}
}

View File

@ -29,13 +29,13 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"]
[dependencies]
actix-codec = "0.4.1"
actix-http = "3.0.0-beta.18"
actix-http = "3.0.0-beta.19"
actix-http-test = "3.0.0-beta.11"
actix-rt = "2.1"
actix-service = "2.0.0"
actix-utils = "3.0.0"
actix-web = { version = "4.0.0-beta.19", default-features = false, features = ["cookies"] }
awc = { version = "3.0.0-beta.18", default-features = false, features = ["cookies"] }
actix-web = { version = "4.0.0-beta.21", default-features = false, features = ["cookies"] }
awc = { version = "3.0.0-beta.19", 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 = [] }

View File

@ -16,8 +16,8 @@ path = "src/lib.rs"
[dependencies]
actix = { version = "0.12.0", default-features = false }
actix-codec = "0.4.1"
actix-http = "3.0.0-beta.18"
actix-web = { version = "4.0.0-beta.19", default-features = false }
actix-http = "3.0.0-beta.19"
actix-web = { version = "4.0.0-beta.21", default-features = false }
bytes = "1"
bytestring = "1"
@ -28,7 +28,7 @@ tokio = { version = "1.8.4", features = ["sync"] }
[dev-dependencies]
actix-rt = "2.2"
actix-test = "0.1.0-beta.11"
awc = { version = "3.0.0-beta.18", default-features = false }
awc = { version = "3.0.0-beta.19", default-features = false }
env_logger = "0.9"
futures-util = { version = "0.3.7", default-features = false }

View File

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

View File

@ -39,13 +39,18 @@
//! ```
//! # use actix_web::HttpResponse;
//! # use actix_web_codegen::route;
//! #[route("/test", method="GET", method="HEAD")]
//! #[route("/test", method = "GET", method = "HEAD")]
//! async fn get_and_head_handler() -> HttpResponse {
//! HttpResponse::Ok().finish()
//! }
//! ```
//!
//! [actix-web attributes docs]: https://docs.rs/actix-web/*/actix_web/#attributes
//! # Multiple Path Handlers
//! There are no macros to generate multi-path handlers. Let us know in [this issue].
//!
//! [this issue]: https://github.com/actix/actix-web/issues/1709
//!
//! [actix-web attributes docs]: https://docs.rs/actix-web/latest/actix_web/#attributes
//! [GET]: macro@get
//! [POST]: macro@post
//! [PUT]: macro@put
@ -73,22 +78,23 @@ mod route;
/// ```
///
/// # Attributes
/// - `"path"` - Raw literal string with path for which to register handler.
/// - `name="resource_name"` - Specifies resource name for the handler. If not set, the function name of handler is used.
/// - `method="HTTP_METHOD"` - Registers HTTP method to provide guard for. Upper-case string, "GET", "POST" for example.
/// - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard`
/// - `wrap="Middleware"` - Registers a resource middleware.
/// - `"path"`: Raw literal string with path for which to register handler.
/// - `name = "resource_name"`: Specifies resource name for the handler. If not set, the function
/// name of handler is used.
/// - `method = "HTTP_METHOD"`: Registers HTTP method to provide guard for. Upper-case string,
/// "GET", "POST" for example.
/// - `guard = "function_name"`: Registers function as guard using `actix_web::guard::fn_guard`.
/// - `wrap = "Middleware"`: Registers a resource middleware.
///
/// # Notes
/// Function name can be specified as any expression that is going to be accessible to the generate
/// code, e.g `my_guard` or `my_module::my_guard`.
///
/// # Example
///
/// # Examples
/// ```
/// # use actix_web::HttpResponse;
/// # use actix_web_codegen::route;
/// #[route("/test", method="GET", method="HEAD")]
/// #[route("/test", method = "GET", method = "HEAD")]
/// async fn example() -> HttpResponse {
/// HttpResponse::Ok().finish()
/// }
@ -98,69 +104,54 @@ pub fn route(args: TokenStream, input: TokenStream) -> TokenStream {
route::with_method(None, args, input)
}
macro_rules! doc_comment {
($x:expr; $($tt:tt)*) => {
#[doc = $x]
$($tt)*
};
}
macro_rules! method_macro {
(
$($variant:ident, $method:ident,)+
) => {
$(doc_comment! {
concat!("
Creates route handler with `actix_web::guard::", stringify!($variant), "`.
# Syntax
```plain
#[", stringify!($method), r#"("path"[, attributes])]
```
# Attributes
- `"path"` - Raw literal string with path for which to register handler.
- `name="resource_name"` - Specifies resource name for the handler. If not set, the function name of handler is used.
- `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard`.
- `wrap="Middleware"` - Registers a resource middleware.
# Notes
Function name can be specified as any expression that is going to be accessible to the generate
code, e.g `my_guard` or `my_module::my_guard`.
# Example
```
# use actix_web::HttpResponse;
# use actix_web_codegen::"#, stringify!($method), ";
#[", stringify!($method), r#"("/")]
async fn example() -> HttpResponse {
HttpResponse::Ok().finish()
($variant:ident, $method:ident) => {
#[doc = concat!("Creates route handler with `actix_web::guard::", stringify!($variant), "`.")]
///
/// # Syntax
/// ```plain
#[doc = concat!("#[", stringify!($method), r#"("path"[, attributes])]"#)]
/// ```
///
/// # Attributes
/// - `"path"`: Raw literal string with path for which to register handler.
/// - `name = "resource_name"`: Specifies resource name for the handler. If not set, the function
/// name of handler is used.
/// - `guard = "function_name"`: Registers function as guard using `actix_web::guard::fn_guard`.
/// - `wrap = "Middleware"`: Registers a resource middleware.
///
/// # Notes
/// Function name can be specified as any expression that is going to be accessible to the
/// generate code, e.g `my_guard` or `my_module::my_guard`.
///
/// # Examples
/// ```
/// # use actix_web::HttpResponse;
#[doc = concat!("# use actix_web_codegen::", stringify!($method), ";")]
#[doc = concat!("#[", stringify!($method), r#"("/")]"#)]
/// async fn example() -> HttpResponse {
/// HttpResponse::Ok().finish()
/// }
/// ```
#[proc_macro_attribute]
pub fn $method(args: TokenStream, input: TokenStream) -> TokenStream {
route::with_method(Some(route::MethodType::$variant), args, input)
}
```
"#);
#[proc_macro_attribute]
pub fn $method(args: TokenStream, input: TokenStream) -> TokenStream {
route::with_method(Some(route::MethodType::$variant), args, input)
}
})+
};
}
method_macro! {
Get, get,
Post, post,
Put, put,
Delete, delete,
Head, head,
Connect, connect,
Options, options,
Trace, trace,
Patch, patch,
}
/// Marks async main function as the actix system entry-point.
method_macro!(Get, get);
method_macro!(Post, post);
method_macro!(Put, put);
method_macro!(Delete, delete);
method_macro!(Head, head);
method_macro!(Connect, connect);
method_macro!(Options, options);
method_macro!(Trace, trace);
method_macro!(Patch, patch);
/// Marks async main function as the Actix Web system entry-point.
///
/// # Examples
/// ```
/// #[actix_web::main]

View File

@ -302,13 +302,13 @@ impl ToTokens for Route {
if methods.len() > 1 {
quote! {
.guard(
actix_web::guard::Any(actix_web::guard::#first())
#(.or(actix_web::guard::#others()))*
::actix_web::guard::Any(::actix_web::guard::#first())
#(.or(::actix_web::guard::#others()))*
)
}
} else {
quote! {
.guard(actix_web::guard::#first())
.guard(::actix_web::guard::#first())
}
}
};
@ -318,17 +318,17 @@ impl ToTokens for Route {
#[allow(non_camel_case_types, missing_docs)]
pub struct #name;
impl actix_web::dev::HttpServiceFactory for #name {
impl ::actix_web::dev::HttpServiceFactory for #name {
fn register(self, __config: &mut actix_web::dev::AppService) {
#ast
let __resource = actix_web::Resource::new(#path)
let __resource = ::actix_web::Resource::new(#path)
.name(#resource_name)
#method_guards
#(.guard(actix_web::guard::fn_guard(#guards)))*
#(.guard(::actix_web::guard::fn_guard(#guards)))*
#(.wrap(#wrappers))*
.#resource_type(#name);
actix_web::dev::HttpServiceFactory::register(__resource, __config)
::actix_web::dev::HttpServiceFactory::register(__resource, __config)
}
}
};

View File

@ -3,6 +3,10 @@
## Unreleased - 2021-xx-xx
## 3.0.0-beta.19 - 2022-01-21
- No significant changes since `3.0.0-beta.18`.
## 3.0.0-beta.18 - 2022-01-04
- Minimum supported Rust version (MSRV) is now 1.54.

View File

@ -1,6 +1,6 @@
[package]
name = "awc"
version = "3.0.0-beta.18"
version = "3.0.0-beta.19"
authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"fakeshadow <24548779@qq.com>",
@ -60,7 +60,7 @@ dangerous-h2c = []
[dependencies]
actix-codec = "0.4.1"
actix-service = "2.0.0"
actix-http = "3.0.0-beta.18"
actix-http = "3.0.0-beta.19"
actix-rt = { version = "2.1", default-features = false }
actix-tls = { version = "3.0.0", features = ["connect", "uri"] }
actix-utils = "3.0.0"
@ -93,15 +93,15 @@ tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features
trust-dns-resolver = { version = "0.20.0", optional = true }
[dev-dependencies]
actix-http = { version = "3.0.0-beta.18", features = ["openssl"] }
actix-http = { version = "3.0.0-beta.19", features = ["openssl"] }
actix-http-test = { version = "3.0.0-beta.11", features = ["openssl"] }
actix-server = "2.0.0-rc.2"
actix-server = "2"
actix-test = { version = "0.1.0-beta.11", features = ["openssl", "rustls"] }
actix-tls = { version = "3.0.0", features = ["openssl", "rustls"] }
actix-utils = "3.0.0"
actix-web = { version = "4.0.0-beta.19", features = ["openssl"] }
actix-web = { version = "4.0.0-beta.21", features = ["openssl"] }
brotli2 = "0.3.2"
brotli = "3.3.3"
const-str = "0.3"
env_logger = "0.9"
flate2 = "1.0.13"
@ -109,6 +109,7 @@ futures-util = { version = "0.3.7", default-features = false }
static_assertions = "1.1"
rcgen = "0.8"
rustls-pemfile = "0.2"
tokio = { version = "1.13.1", features = ["rt-multi-thread", "macros"] }
zstd = "0.9"
[[example]]

View File

@ -3,9 +3,9 @@
> Async HTTP and WebSocket client library.
[![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc)
[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.18)](https://docs.rs/awc/3.0.0-beta.18)
[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.19)](https://docs.rs/awc/3.0.0-beta.19)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc)
[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.18/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.18)
[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.19/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.19)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
## Documentation & Resources

View File

@ -1,13 +1,13 @@
use std::error::Error as StdError;
#[actix_web::main]
#[tokio::main]
async fn main() -> Result<(), Box<dyn StdError>> {
std::env::set_var("RUST_LOG", "client=trace,awc=trace,actix_http=trace");
env_logger::init();
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
// construct request builder
let client = awc::Client::new();
// Create request builder, configure request and send
// configure request
let request = client
.get("https://www.rust-lang.org/")
.append_header(("User-Agent", "Actix-web"));
@ -16,7 +16,7 @@ async fn main() -> Result<(), Box<dyn StdError>> {
let mut response = request.send().await?;
// server http response
// server response head
println!("Response: {:?}", response);
// read response body

View File

@ -207,7 +207,7 @@ where
self
}
/// Maximum supported HTTP major version.
/// Sets maximum supported HTTP major version.
///
/// Supported versions are HTTP/1.1 and HTTP/2.
pub fn max_http_version(mut self, val: http::Version) -> Self {
@ -222,8 +222,8 @@ where
self
}
/// Indicates the initial window size (in octets) for
/// HTTP2 stream-level flow control for received data.
/// Sets the initial window size (in octets) for HTTP/2 stream-level flow control for
/// received data.
///
/// The default value is 65,535 and is good for APIs, but not for big objects.
pub fn initial_window_size(mut self, size: u32) -> Self {
@ -231,8 +231,8 @@ where
self
}
/// Indicates the initial window size (in octets) for
/// HTTP2 connection-level flow control for received data.
/// Sets the initial window size (in octets) for HTTP/2 connection-level flow control for
/// received data.
///
/// The default value is 65,535 and is good for APIs, but not for big objects.
pub fn initial_connection_window_size(mut self, size: u32) -> Self {
@ -243,6 +243,7 @@ where
/// Set total number of simultaneous connections per type of scheme.
///
/// If limit is 0, the connector has no limit.
///
/// The default limit size is 100.
pub fn limit(mut self, limit: usize) -> Self {
self.config.limit = limit;

View File

@ -67,12 +67,13 @@ impl Default for Client {
}
impl Client {
/// Create new client instance with default settings.
/// Constructs new client instance with default settings.
pub fn new() -> Client {
Client::default()
}
/// Create `Client` builder.
/// Constructs new `Client` builder.
///
/// This function is equivalent of `ClientBuilder::new()`.
pub fn builder() -> ClientBuilder<
impl Service<

View File

@ -1,5 +1,5 @@
use std::{
cell::{Ref, RefMut},
cell::{Ref, RefCell, RefMut},
fmt, mem,
pin::Pin,
task::{Context, Poll},
@ -28,6 +28,8 @@ pin_project! {
#[pin]
pub(crate) payload: Payload<S>,
pub(crate) timeout: ResponseTimeout,
pub(crate) extensions: RefCell<Extensions>,
}
}
@ -38,6 +40,7 @@ impl<S> ClientResponse<S> {
head,
payload,
timeout: ResponseTimeout::default(),
extensions: RefCell::new(Extensions::new()),
}
}
@ -64,7 +67,9 @@ impl<S> ClientResponse<S> {
&self.head().headers
}
/// Set a body and return previous body value
/// Map the current body type to another using a closure. Returns a new response.
///
/// Closure receives the response head and the current body type.
pub fn map_body<F, U>(mut self, f: F) -> ClientResponse<U>
where
F: FnOnce(&mut ResponseHead, Payload<S>) -> Payload<U>,
@ -75,6 +80,7 @@ impl<S> ClientResponse<S> {
payload,
head: self.head,
timeout: self.timeout,
extensions: self.extensions,
}
}
@ -101,6 +107,7 @@ impl<S> ClientResponse<S> {
payload: self.payload,
head: self.head,
timeout,
extensions: self.extensions,
}
}
@ -153,7 +160,6 @@ where
///
/// # Errors
/// `Future` implementation returns error if:
/// - content type is not `application/json`
/// - content length is greater than [limit](JsonBody::limit) (default: 2 MiB)
///
/// # Examples
@ -224,11 +230,11 @@ impl<S> HttpMessage for ClientResponse<S> {
}
fn extensions(&self) -> Ref<'_, Extensions> {
self.head.extensions()
self.extensions.borrow()
}
fn extensions_mut(&self) -> RefMut<'_, Extensions> {
self.head.extensions_mut()
self.extensions.borrow_mut()
}
}

View File

@ -2,7 +2,7 @@
//!
//! Type definitions required to use [`awc::Client`](super::Client) as a WebSocket client.
//!
//! # Example
//! # Examples
//!
//! ```no_run
//! use awc::{Client, ws};

View File

@ -41,16 +41,22 @@ pub mod deflate {
pub mod brotli {
use super::*;
use ::brotli2::{read::BrotliDecoder, write::BrotliEncoder};
use ::brotli::{reader::Decompressor as BrotliDecoder, CompressorWriter as BrotliEncoder};
pub fn encode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
let mut encoder = BrotliEncoder::new(Vec::new(), 3);
let mut encoder = BrotliEncoder::new(
Vec::new(),
8 * 1024, // 32 KiB buffer
3, // BROTLI_PARAM_QUALITY
22, // BROTLI_PARAM_LGWIN
);
encoder.write_all(bytes.as_ref()).unwrap();
encoder.finish().unwrap()
encoder.flush().unwrap();
encoder.into_inner()
}
pub fn decode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
let mut decoder = BrotliDecoder::new(bytes.as_ref());
let mut decoder = BrotliDecoder::new(bytes.as_ref(), 8_096);
let mut buf = Vec::new();
decoder.read_to_end(&mut buf).unwrap();
buf

View File

@ -24,7 +24,7 @@ async fn main() -> std::io::Result<()> {
App::new()
.wrap(middleware::DefaultHeaders::new().add(("X-Version", "0.2")))
.wrap(middleware::Compress::default())
.wrap(middleware::Logger::default())
.wrap(middleware::Logger::default().log_target("http_log"))
.service(index)
.service(no_params)
.service(

View File

@ -17,12 +17,21 @@ if [ "$(uname)" = "Darwin" ]; then
fi
CARGO_MANIFEST=$DIR/Cargo.toml
CHANGELOG_FILE=$DIR/CHANGES.md
README_FILE=$DIR/README.md
# determine changelog file name
if [ -f "$DIR/CHANGES.md" ]; then
CHANGELOG_FILE=$DIR/CHANGES.md
elif [ -f "$DIR/CHANGELOG.md" ]; then
CHANGELOG_FILE=$DIR/CHANGELOG.md
else
echo "No changelog file found"
exit 1
fi
# get current version
PACKAGE_NAME="$(sed -nE 's/^name ?= ?"([^"]+)"$/\1/ p' "$CARGO_MANIFEST" | head -n 1)"
CURRENT_VERSION="$(sed -nE 's/^version ?= ?"([^"]+)"$/\1/ p' "$CARGO_MANIFEST")"
CURRENT_VERSION="$(sed -nE 's/^version ?= ?"([^"]+)"$/\1/ p' "$CARGO_MANIFEST" | head -n 1)"
CHANGE_CHUNK_FILE="$(mktemp)"
echo saving changelog to $CHANGE_CHUNK_FILE

View File

@ -51,7 +51,10 @@ impl App<AppEntry> {
}
}
impl<T> App<T> {
impl<T> App<T>
where
T: ServiceFactory<ServiceRequest, Config = (), Error = Error, InitError = ()>,
{
/// Set application (root level) data.
///
/// Application data stored with `App::app_data()` method is available through the
@ -317,65 +320,63 @@ impl<T> App<T> {
self
}
/// Registers middleware, in the form of a middleware component (type),
/// that runs during inbound and/or outbound processing in the request
/// life-cycle (request -> response), modifying request/response as
/// necessary, across all requests managed by the *Application*.
/// Registers an app-wide middleware.
///
/// Use middleware when you need to read or modify *every* request or
/// response in some way.
/// Registers middleware, in the form of a middleware compo nen t (type), that runs during
/// inbound and/or outbound processing in the request life-cycle (request -> response),
/// modifying request/response as necessary, across all requests managed by the `App`.
///
/// Notice that the keyword for registering middleware is `wrap`. As you
/// register middleware using `wrap` in the App builder, imagine wrapping
/// layers around an inner App. The first middleware layer exposed to a
/// Request is the outermost layer-- the *last* registered in
/// the builder chain. Consequently, the *first* middleware registered
/// in the builder chain is the *last* to execute during request processing.
/// Use middleware when you need to read or modify *every* request or response in some way.
///
/// Middleware can be applied similarly to individual `Scope`s and `Resource`s.
/// See [`Scope::wrap`](crate::Scope::wrap) and [`Resource::wrap`].
///
/// # Middleware Order
/// Notice that the keyword for registering middleware is `wrap`. As you register middleware
/// using `wrap` in the App builder, imagine wrapping layers around an inner App. The first
/// middleware layer exposed to a Request is the outermost layer (i.e., the *last* registered in
/// the builder chain). Consequently, the *first* middleware registered in the builder chain is
/// the *last* to start executing during request processing.
///
/// Ordering is less obvious when wrapped services also have middleware applied. In this case,
/// middlewares are run in reverse order for `App` _and then_ in reverse order for the
/// wrapped service.
///
/// # Examples
/// ```
/// use actix_service::Service;
/// use actix_web::{middleware, web, App};
/// use actix_web::http::header::{CONTENT_TYPE, HeaderValue};
///
/// async fn index() -> &'static str {
/// "Welcome!"
/// }
///
/// fn main() {
/// let app = App::new()
/// .wrap(middleware::Logger::default())
/// .route("/index.html", web::get().to(index));
/// }
/// let app = App::new()
/// .wrap(middleware::Logger::default())
/// .route("/index.html", web::get().to(index));
/// ```
pub fn wrap<M, B, B1>(
#[doc(alias = "middleware")]
#[doc(alias = "use")] // nodejs terminology
pub fn wrap<M, B>(
self,
mw: M,
) -> App<
impl ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<B1>,
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
>,
>
where
T: ServiceFactory<
ServiceRequest,
Response = ServiceResponse<B>,
Error = Error,
Config = (),
InitError = (),
>,
B: MessageBody,
M: Transform<
T::Service,
ServiceRequest,
Response = ServiceResponse<B1>,
Error = Error,
InitError = (),
>,
B1: MessageBody,
T::Service,
ServiceRequest,
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
> + 'static,
B: MessageBody,
{
App {
endpoint: apply(mw, self.endpoint),
@ -388,61 +389,57 @@ impl<T> App<T> {
}
}
/// Registers middleware, in the form of a closure, that runs during inbound
/// and/or outbound processing in the request life-cycle (request -> response),
/// modifying request/response as necessary, across all requests managed by
/// the *Application*.
/// Registers an app-wide function middleware.
///
/// `mw` is a closure that runs during inbound and/or outbound processing in the request
/// life-cycle (request -> response), modifying request/response as necessary, across all
/// requests handled by the `App`.
///
/// Use middleware when you need to read or modify *every* request or response in some way.
///
/// Middleware can also be applied to individual `Scope`s and `Resource`s.
///
/// See [`App::wrap`] for details on how middlewares compose with each other.
///
/// # Examples
/// ```
/// use actix_service::Service;
/// use actix_web::{web, App};
/// use actix_web::{dev::Service as _, middleware, web, App};
/// use actix_web::http::header::{CONTENT_TYPE, HeaderValue};
///
/// async fn index() -> &'static str {
/// "Welcome!"
/// }
///
/// fn main() {
/// let app = App::new()
/// .wrap_fn(|req, srv| {
/// let fut = srv.call(req);
/// async {
/// let mut res = fut.await?;
/// res.headers_mut().insert(
/// CONTENT_TYPE, HeaderValue::from_static("text/plain"),
/// );
/// Ok(res)
/// }
/// })
/// .route("/index.html", web::get().to(index));
/// }
/// let app = App::new()
/// .wrap_fn(|req, srv| {
/// let fut = srv.call(req);
/// async {
/// let mut res = fut.await?;
/// res.headers_mut()
/// .insert(CONTENT_TYPE, HeaderValue::from_static("text/plain"));
/// Ok(res)
/// }
/// })
/// .route("/index.html", web::get().to(index));
/// ```
pub fn wrap_fn<F, R, B, B1>(
#[doc(alias = "middleware")]
#[doc(alias = "use")] // nodejs terminology
pub fn wrap_fn<F, R, B>(
self,
mw: F,
) -> App<
impl ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<B1>,
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
>,
>
where
T: ServiceFactory<
ServiceRequest,
Response = ServiceResponse<B>,
Error = Error,
Config = (),
InitError = (),
>,
F: Fn(ServiceRequest, &T::Service) -> R + Clone + 'static,
R: Future<Output = Result<ServiceResponse<B>, Error>>,
B: MessageBody,
F: Fn(ServiceRequest, &T::Service) -> R + Clone,
R: Future<Output = Result<ServiceResponse<B1>, Error>>,
B1: MessageBody,
{
App {
endpoint: apply_fn_factory(self.endpoint, mw),
@ -458,15 +455,14 @@ impl<T> App<T> {
impl<T, B> IntoServiceFactory<AppInit<T, B>, Request> for App<T>
where
B: MessageBody,
T: ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
>,
T::Future: 'static,
ServiceRequest,
Config = (),
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
> + 'static,
B: MessageBody,
{
fn into_factory(self) -> AppInit<T, B> {
AppInit {

View File

@ -201,27 +201,29 @@ where
actix_service::forward_ready!(service);
fn call(&self, mut req: Request) -> Self::Future {
let req_data = Rc::new(RefCell::new(req.take_req_data()));
let extensions = Rc::new(RefCell::new(req.take_req_data()));
let conn_data = req.take_conn_data();
let (head, payload) = req.into_parts();
let req = if let Some(mut req) = self.app_state.pool().pop() {
let inner = Rc::get_mut(&mut req.inner).unwrap();
inner.path.get_mut().update(&head.uri);
inner.path.reset();
inner.head = head;
inner.conn_data = conn_data;
inner.req_data = req_data;
req
} else {
HttpRequest::new(
let req = match self.app_state.pool().pop() {
Some(mut req) => {
let inner = Rc::get_mut(&mut req.inner).unwrap();
inner.path.get_mut().update(&head.uri);
inner.path.reset();
inner.head = head;
inner.conn_data = conn_data;
inner.extensions = extensions;
req
}
None => HttpRequest::new(
Path::new(Url::new(head.uri.clone())),
head,
self.app_state.clone(),
self.app_data.clone(),
Rc::clone(&self.app_state),
Rc::clone(&self.app_data),
conn_data,
req_data,
)
extensions,
),
};
self.service.call(ServiceRequest::new(req, payload))

View File

@ -215,6 +215,17 @@ impl ServiceConfig {
self
}
/// Run external configuration as part of the application building process
///
/// Counterpart to [`App::configure()`](crate::App::configure) that allows for easy nesting.
pub fn configure<F>(&mut self, f: F) -> &mut Self
where
F: FnOnce(&mut ServiceConfig),
{
f(self);
self
}
/// Configure route for a specific path.
///
/// Counterpart to [`App::route()`](crate::App::route).
@ -264,7 +275,7 @@ mod tests {
use super::*;
use crate::http::{Method, StatusCode};
use crate::test::{call_service, init_service, read_body, TestRequest};
use crate::test::{assert_body_eq, call_service, init_service, read_body, TestRequest};
use crate::{web, App, HttpRequest, HttpResponse};
// allow deprecated `ServiceConfig::data`
@ -288,38 +299,6 @@ mod tests {
assert_eq!(resp.status(), StatusCode::OK);
}
// #[actix_rt::test]
// async fn test_data_factory() {
// let cfg = |cfg: &mut ServiceConfig| {
// cfg.data_factory(|| {
// sleep(std::time::Duration::from_millis(50)).then(|_| {
// println!("READY");
// Ok::<_, ()>(10usize)
// })
// });
// };
// let srv =
// init_service(App::new().configure(cfg).service(
// web::resource("/").to(|_: web::Data<usize>| HttpResponse::Ok()),
// ));
// let req = TestRequest::default().to_request();
// let resp = srv.call(req).await.unwrap();
// assert_eq!(resp.status(), StatusCode::OK);
// let cfg2 = |cfg: &mut ServiceConfig| {
// cfg.data_factory(|| Ok::<_, ()>(10u32));
// };
// let srv = init_service(
// App::new()
// .service(web::resource("/").to(|_: web::Data<usize>| HttpResponse::Ok()))
// .configure(cfg2),
// );
// let req = TestRequest::default().to_request();
// let resp = srv.call(req).await.unwrap();
// assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
// }
#[actix_rt::test]
async fn test_external_resource() {
let srv = init_service(
@ -363,4 +342,22 @@ mod tests {
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn nested_service_configure() {
fn cfg_root(cfg: &mut ServiceConfig) {
cfg.configure(cfg_sub);
}
fn cfg_sub(cfg: &mut ServiceConfig) {
cfg.route("/", web::get().to(|| async { "hello world" }));
}
let srv = init_service(App::new().configure(cfg_root)).await;
let req = TestRequest::with_uri("/").to_request();
let res = call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert_body_eq!(res, b"hello world");
}
}

View File

@ -3,6 +3,7 @@
use std::{
convert::Infallible,
future::Future,
marker::PhantomData,
pin::Pin,
task::{Context, Poll},
};
@ -124,12 +125,11 @@ pub trait FromRequest: Sized {
/// );
/// }
/// ```
impl<T: 'static> FromRequest for Option<T>
impl<T> FromRequest for Option<T>
where
T: FromRequest,
T::Future: 'static,
{
type Error = Error;
type Error = Infallible;
type Future = FromRequestOptFuture<T::Future>;
#[inline]
@ -152,7 +152,7 @@ where
Fut: Future<Output = Result<T, E>>,
E: Into<Error>,
{
type Output = Result<Option<T>, Error>;
type Output = Result<Option<T>, Infallible>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
@ -211,40 +211,42 @@ where
/// );
/// }
/// ```
impl<T> FromRequest for Result<T, T::Error>
impl<T, E> FromRequest for Result<T, E>
where
T: FromRequest + 'static,
T::Error: 'static,
T::Future: 'static,
T: FromRequest,
T::Error: Into<E>,
{
type Error = Error;
type Future = FromRequestResFuture<T::Future>;
type Error = Infallible;
type Future = FromRequestResFuture<T::Future, E>;
#[inline]
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
FromRequestResFuture {
fut: T::from_request(req, payload),
_phantom: PhantomData,
}
}
}
pin_project! {
pub struct FromRequestResFuture<Fut> {
pub struct FromRequestResFuture<Fut, E> {
#[pin]
fut: Fut,
_phantom: PhantomData<E>,
}
}
impl<Fut, T, E> Future for FromRequestResFuture<Fut>
impl<Fut, T, Ei, E> Future for FromRequestResFuture<Fut, E>
where
Fut: Future<Output = Result<T, E>>,
Fut: Future<Output = Result<T, Ei>>,
Ei: Into<E>,
{
type Output = Result<Result<T, E>, Error>;
type Output = Result<Result<T, E>, Infallible>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
let res = ready!(this.fut.poll(cx));
Poll::Ready(Ok(res))
Poll::Ready(Ok(res.map_err(Into::into)))
}
}
@ -290,16 +292,6 @@ impl FromRequest for Method {
}
}
#[doc(hidden)]
impl FromRequest for () {
type Error = Infallible;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(_: &HttpRequest, _: &mut Payload) -> Self::Future {
ok(())
}
}
#[doc(hidden)]
#[allow(non_snake_case)]
mod tuple_from_req {
@ -388,6 +380,15 @@ mod tuple_from_req {
}
}
impl FromRequest for () {
type Error = Infallible;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(_: &HttpRequest, _: &mut Payload) -> Self::Future {
ok(())
}
}
tuple_from_req! { TupleFromRequest1; A }
tuple_from_req! { TupleFromRequest2; A, B }
tuple_from_req! { TupleFromRequest3; A, B, C }
@ -398,6 +399,8 @@ mod tuple_from_req {
tuple_from_req! { TupleFromRequest8; A, B, C, D, E, F, G, H }
tuple_from_req! { TupleFromRequest9; A, B, C, D, E, F, G, H, I }
tuple_from_req! { TupleFromRequest10; A, B, C, D, E, F, G, H, I, J }
tuple_from_req! { TupleFromRequest11; A, B, C, D, E, F, G, H, I, J, K }
tuple_from_req! { TupleFromRequest12; A, B, C, D, E, F, G, H, I, J, K, L }
}
#[cfg(test)]
@ -480,7 +483,14 @@ mod tests {
.set_payload(Bytes::from_static(b"bye=world"))
.to_http_parts();
let r = Result::<Form<Info>, Error>::from_request(&req, &mut pl)
struct MyError;
impl From<Error> for MyError {
fn from(_: Error) -> Self {
Self
}
}
let r = Result::<Form<Info>, MyError>::from_request(&req, &mut pl)
.await
.unwrap();
assert!(r.is_err());

View File

@ -54,7 +54,7 @@ use std::{
use actix_http::{header, uri::Uri, Extensions, Method as HttpMethod, RequestHead};
use crate::service::ServiceRequest;
use crate::{http::header::Header, service::ServiceRequest, HttpMessage as _};
/// Provides access to request parts that are useful during routing.
#[derive(Debug)]
@ -69,16 +69,36 @@ impl<'a> GuardContext<'a> {
self.req.head()
}
/// Returns reference to the request-local data container.
/// Returns reference to the request-local data/extensions container.
#[inline]
pub fn req_data(&self) -> Ref<'a, Extensions> {
self.req.req_data()
self.req.extensions()
}
/// Returns mutable reference to the request-local data container.
/// Returns mutable reference to the request-local data/extensions container.
#[inline]
pub fn req_data_mut(&self) -> RefMut<'a, Extensions> {
self.req.req_data_mut()
self.req.extensions_mut()
}
/// Extracts a typed header from the request.
///
/// Returns `None` if parsing `H` fails.
///
/// # Examples
/// ```
/// use actix_web::{guard::fn_guard, http::header};
///
/// let image_accept_guard = fn_guard(|ctx| {
/// match ctx.header::<header::Accept>() {
/// Some(hdr) => hdr.preference() == "image/*",
/// None => false,
/// }
/// });
/// ```
#[inline]
pub fn header<H: Header>(&self) -> Option<H> {
H::parse(self.req).ok()
}
}

View File

@ -12,17 +12,14 @@ use crate::{
/// # What Is A Request Handler
/// A request handler has three requirements:
/// 1. It is an async function (or a function/closure that returns an appropriate future);
/// 1. The function accepts zero or more parameters that implement [`FromRequest`];
/// 1. The function parameters (up to 12) implement [`FromRequest`];
/// 1. The async function (or future) resolves to a type that can be converted into an
/// [`HttpResponse`] (i.e., it implements the [`Responder`] trait).
///
/// # Compiler Errors
/// If you get the error `the trait Handler<_> is not implemented`, then your handler does not
/// fulfill one or more of the above requirements.
///
/// Unfortunately we cannot provide a better compile error message (while keeping the trait's
/// flexibility) unless a stable alternative to [`#[rustc_on_unimplemented]`][on_unimpl] is added
/// to Rust.
/// fulfill the _first_ of the above requirements. Missing other requirements manifest as errors on
/// implementing [`FromRequest`] and [`Responder`], respectively.
///
/// # How Do Handlers Receive Variable Numbers Of Arguments
/// Rest assured there is no macro magic here; it's just traits.
@ -62,13 +59,15 @@ use crate::{
/// This is the source code for the 2-parameter implementation of `Handler` to help illustrate the
/// bounds of the handler call after argument extraction:
/// ```ignore
/// impl<Func, Arg1, Arg2, R> Handler<(Arg1, Arg2), R> for Func
/// impl<Func, Arg1, Arg2, Fut> Handler<(Arg1, Arg2)> for Func
/// where
/// Func: Fn(Arg1, Arg2) -> R + Clone + 'static,
/// R: Future,
/// R::Output: Responder,
/// Func: Fn(Arg1, Arg2) -> Fut + Clone + 'static,
/// Fut: Future,
/// {
/// fn call(&self, (arg1, arg2): (Arg1, Arg2)) -> R {
/// type Output = Fut::Output;
/// type Future = Fut;
///
/// fn call(&self, (arg1, arg2): (Arg1, Arg2)) -> Self::Future {
/// (self)(arg1, arg2)
/// }
/// }
@ -76,7 +75,6 @@ use crate::{
///
/// [arity]: https://en.wikipedia.org/wiki/Arity
/// [`from_request`]: FromRequest::from_request
/// [on_unimpl]: https://github.com/rust-lang/rust/issues/29628
pub trait Handler<Args>: Clone + 'static {
type Output;
type Future: Future<Output = Self::Output>;
@ -121,8 +119,9 @@ where
/// ```
macro_rules! factory_tuple ({ $($param:ident)* } => {
impl<Func, Fut, $($param,)*> Handler<($($param,)*)> for Func
where Func: Fn($($param),*) -> Fut + Clone + 'static,
Fut: Future,
where
Func: Fn($($param),*) -> Fut + Clone + 'static,
Fut: Future,
{
type Output = Fut::Output;
type Future = Fut;
@ -148,3 +147,25 @@ factory_tuple! { A B C D E F G H I }
factory_tuple! { A B C D E F G H I J }
factory_tuple! { A B C D E F G H I J K }
factory_tuple! { A B C D E F G H I J K L }
#[cfg(test)]
mod tests {
use super::*;
fn assert_impl_handler<T: FromRequest>(_: impl Handler<T>) {}
#[test]
fn arg_number() {
async fn handler_min() {}
#[rustfmt::skip]
#[allow(clippy::too_many_arguments, clippy::just_underscores_and_digits)]
async fn handler_max(
_01: (), _02: (), _03: (), _04: (), _05: (), _06: (),
_07: (), _08: (), _09: (), _10: (), _11: (), _12: (),
) {}
assert_impl_handler(handler_min);
assert_impl_handler(handler_max);
}
}

View File

@ -2,10 +2,10 @@ use std::cmp::Ordering;
use mime::Mime;
use super::QualityItem;
use super::{common_header, QualityItem};
use crate::http::header;
crate::http::header::common_header! {
common_header! {
/// `Accept` header, defined
/// in [RFC 7231 §5.3.2](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2)
///

View File

@ -16,7 +16,7 @@ crate::http::header::common_header! {
/// # Example Values
/// * `Tue, 15 Nov 1994 08:12:31 GMT`
///
/// # Example
/// # Examples
///
/// ```
/// use std::time::SystemTime;

View File

@ -19,7 +19,7 @@ crate::http::header::common_header! {
/// # Example Values
/// * `Thu, 01 Dec 1994 16:00:00 GMT`
///
/// # Example
/// # Examples
///
/// ```
/// use std::time::{SystemTime, Duration};

View File

@ -18,7 +18,7 @@ crate::http::header::common_header! {
/// # Example Values
/// * `Sat, 29 Oct 1994 19:43:31 GMT`
///
/// # Example
/// # Examples
///
/// ```
/// use std::time::{SystemTime, Duration};

View File

@ -18,7 +18,7 @@ crate::http::header::common_header! {
/// # Example Values
/// * `Sat, 29 Oct 1994 19:43:31 GMT`
///
/// # Example
/// # Examples
///
/// ```
/// use std::time::{SystemTime, Duration};

View File

@ -17,7 +17,7 @@ crate::http::header::common_header! {
/// # Example Values
/// * `Sat, 29 Oct 1994 19:43:31 GMT`
///
/// # Example
/// # Examples
///
/// ```
/// use std::time::{SystemTime, Duration};

View File

@ -150,11 +150,13 @@ mod tests {
use actix_service::IntoService;
use crate::dev::ServiceRequest;
use crate::http::StatusCode;
use crate::middleware::{self, Condition, Logger};
use crate::test::{call_service, init_service, TestRequest};
use crate::{web, App, HttpResponse};
use crate::{
dev::ServiceRequest,
http::StatusCode,
middleware::{self, Condition, Logger},
test::{self, call_service, init_service, TestRequest},
web, App, HttpResponse,
};
#[actix_rt::test]
#[cfg(all(feature = "cookies", feature = "__compress"))]
@ -219,4 +221,17 @@ mod tests {
let resp = call_service(&mw, TestRequest::default().to_srv_request()).await;
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
#[actix_rt::test]
async fn compat_noop_is_noop() {
let srv = test::ok_service();
let mw = Compat::noop()
.new_transform(srv.into_service())
.await
.unwrap();
let resp = call_service(&mw, TestRequest::default().to_srv_request()).await;
assert_eq!(resp.status(), StatusCode::OK);
}
}

View File

@ -1,6 +1,7 @@
//! For middleware documentation, see [`Logger`].
use std::{
borrow::Cow,
collections::HashSet,
convert::TryFrom,
env,
@ -87,6 +88,7 @@ struct Inner {
format: Format,
exclude: HashSet<String>,
exclude_regex: RegexSet,
log_target: Cow<'static, str>,
}
impl Logger {
@ -96,6 +98,7 @@ impl Logger {
format: Format::new(format),
exclude: HashSet::new(),
exclude_regex: RegexSet::empty(),
log_target: Cow::Borrowed(module_path!()),
}))
}
@ -118,13 +121,31 @@ impl Logger {
self
}
/// Sets the logging target to `target`.
///
/// By default, the log target is `module_path!()` of the log call location. In our case, that
/// would be `actix_web::middleware::logger`.
///
/// # Examples
/// Using `.log_target("http_log")` would have this effect on request logs:
/// ```diff
/// - [2015-10-21T07:28:00Z INFO actix_web::middleware::logger] 127.0.0.1 "GET / HTTP/1.1" 200 88 "-" "dmc/1.0" 0.001985
/// + [2015-10-21T07:28:00Z INFO http_log] 127.0.0.1 "GET / HTTP/1.1" 200 88 "-" "dmc/1.0" 0.001985
/// ^^^^^^^^
/// ```
pub fn log_target(mut self, target: impl Into<Cow<'static, str>>) -> Self {
let inner = Rc::get_mut(&mut self.0).unwrap();
inner.log_target = target.into();
self
}
/// Register a function that receives a ServiceRequest and returns a String for use in the
/// log line. The label passed as the first argument should match a replacement substring in
/// the logger format like `%{label}xi`.
///
/// It is convention to print "-" to indicate no output instead of an empty string.
///
/// # Example
/// # Examples
/// ```
/// # use actix_web::http::{header::HeaderValue};
/// # use actix_web::middleware::Logger;
@ -171,6 +192,7 @@ impl Default for Logger {
format: Format::default(),
exclude: HashSet::new(),
exclude_regex: RegexSet::empty(),
log_target: Cow::Borrowed(module_path!()),
}))
}
}
@ -222,13 +244,15 @@ where
actix_service::forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
if self.inner.exclude.contains(req.path())
|| self.inner.exclude_regex.is_match(req.path())
{
let excluded = self.inner.exclude.contains(req.path())
|| self.inner.exclude_regex.is_match(req.path());
if excluded {
LoggerResponse {
fut: self.service.call(req),
format: None,
time: OffsetDateTime::now_utc(),
log_target: Cow::Borrowed(""),
_phantom: PhantomData,
}
} else {
@ -238,10 +262,12 @@ where
for unit in &mut format.0 {
unit.render_request(now, &req);
}
LoggerResponse {
fut: self.service.call(req),
format: Some(format),
time: now,
log_target: self.inner.log_target.clone(),
_phantom: PhantomData,
}
}
@ -258,6 +284,7 @@ pin_project! {
fut: S::Future,
time: OffsetDateTime,
format: Option<Format>,
log_target: Cow<'static, str>,
_phantom: PhantomData<B>,
}
}
@ -289,12 +316,14 @@ where
let time = *this.time;
let format = this.format.take();
let log_target = this.log_target.clone();
Poll::Ready(Ok(res.map_body(move |_, body| StreamLog {
body,
time,
format,
size: 0,
log_target,
})))
}
}
@ -306,7 +335,9 @@ pin_project! {
format: Option<Format>,
size: usize,
time: OffsetDateTime,
log_target: Cow<'static, str>,
}
impl<B> PinnedDrop for StreamLog<B> {
fn drop(this: Pin<&mut Self>) {
if let Some(ref format) = this.format {
@ -316,7 +347,11 @@ pin_project! {
}
Ok(())
};
log::info!("{}", FormatDisplay(&render));
log::info!(
target: this.log_target.as_ref(),
"{}", FormatDisplay(&render)
);
}
}
}
@ -700,7 +735,6 @@ mod tests {
Ok(())
};
let s = format!("{}", FormatDisplay(&render));
println!("{}", s);
assert!(s.contains("/test/route/yeah"));
}
@ -794,7 +828,6 @@ mod tests {
Ok(())
};
let s = format!("{}", FormatDisplay(&render));
println!("{}", s);
assert!(s.contains("192.0.2.60"));
}

View File

@ -1,4 +1,4 @@
//! Commonly used middleware.
//! A collection of common middleware.
mod compat;
mod condition;

View File

@ -5,10 +5,7 @@ use std::{
str,
};
use actix_http::{
header::HeaderMap, Extensions, HttpMessage, Message, Method, Payload, RequestHead, Uri,
Version,
};
use actix_http::{Message, RequestHead};
use actix_router::{Path, Url};
use actix_utils::future::{ok, Ready};
#[cfg(feature = "cookies")]
@ -16,8 +13,14 @@ use cookie::{Cookie, ParseError as CookieParseError};
use smallvec::SmallVec;
use crate::{
app_service::AppInitServiceState, config::AppConfig, error::UrlGenerationError,
info::ConnectionInfo, rmap::ResourceMap, Error, FromRequest,
app_service::AppInitServiceState,
config::AppConfig,
dev::{Extensions, Payload},
error::UrlGenerationError,
http::{header::HeaderMap, Method, Uri, Version},
info::ConnectionInfo,
rmap::ResourceMap,
Error, FromRequest, HttpMessage,
};
#[cfg(feature = "cookies")]
@ -38,7 +41,7 @@ pub(crate) struct HttpRequestInner {
pub(crate) path: Path<Url>,
pub(crate) app_data: SmallVec<[Rc<Extensions>; 4]>,
pub(crate) conn_data: Option<Rc<Extensions>>,
pub(crate) req_data: Rc<RefCell<Extensions>>,
pub(crate) extensions: Rc<RefCell<Extensions>>,
app_state: Rc<AppInitServiceState>,
}
@ -50,7 +53,7 @@ impl HttpRequest {
app_state: Rc<AppInitServiceState>,
app_data: Rc<Extensions>,
conn_data: Option<Rc<Extensions>>,
req_data: Rc<RefCell<Extensions>>,
extensions: Rc<RefCell<Extensions>>,
) -> HttpRequest {
let mut data = SmallVec::<[Rc<Extensions>; 4]>::new();
data.push(app_data);
@ -62,7 +65,7 @@ impl HttpRequest {
app_state,
app_data: data,
conn_data,
req_data,
extensions,
}),
}
}
@ -135,6 +138,10 @@ impl HttpRequest {
&self.inner.path
}
/// Returns a mutable reference to the URL parameters container.
///
/// # Panics
/// Panics if this `HttpRequest` has been cloned.
#[inline]
pub(crate) fn match_info_mut(&mut self) -> &mut Path<Url> {
&mut Rc::get_mut(&mut self.inner).unwrap().path
@ -159,14 +166,6 @@ impl HttpRequest {
self.resource_map().match_name(self.path())
}
pub fn req_data(&self) -> Ref<'_, Extensions> {
self.inner.req_data.borrow()
}
pub fn req_data_mut(&self) -> RefMut<'_, Extensions> {
self.inner.req_data.borrow_mut()
}
/// Returns a reference a piece of connection data set in an [on-connect] callback.
///
/// ```ignore
@ -213,7 +212,7 @@ impl HttpRequest {
self.resource_map().url_for(self, name, elements)
}
/// Generate url for named resource
/// Generate URL for named resource
///
/// This method is similar to `HttpRequest::url_for()` but it can be used
/// for urls that do not contain variable parts.
@ -356,12 +355,12 @@ impl HttpMessage for HttpRequest {
#[inline]
fn extensions(&self) -> Ref<'_, Extensions> {
self.req_data()
self.inner.extensions.borrow()
}
#[inline]
fn extensions_mut(&self) -> RefMut<'_, Extensions> {
self.req_data_mut()
self.inner.extensions.borrow_mut()
}
#[inline]
@ -382,7 +381,10 @@ impl Drop for HttpRequest {
// Inner is borrowed mut here and; get req data mutably to reduce borrow check. Also
// we know the req_data Rc will not have any cloned at this point to unwrap is okay.
Rc::get_mut(&mut inner.req_data).unwrap().get_mut().clear();
Rc::get_mut(&mut inner.extensions)
.unwrap()
.get_mut()
.clear();
// a re-borrow of pool is necessary here.
let req = Rc::clone(&self.inner);
@ -506,10 +508,12 @@ mod tests {
use bytes::Bytes;
use super::*;
use crate::dev::{ResourceDef, ResourceMap};
use crate::http::{header, StatusCode};
use crate::test::{call_service, init_service, read_body, TestRequest};
use crate::{web, App, HttpResponse};
use crate::{
dev::{ResourceDef, ResourceMap},
http::{header, StatusCode},
test::{self, call_service, init_service, read_body, TestRequest},
web, App, HttpResponse,
};
#[test]
fn test_debug() {
@ -863,4 +867,47 @@ mod tests {
let res = call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn url_for_closest_named_resource() {
// we mount the route named 'nested' on 2 different scopes, 'a' and 'b'
let srv = test::init_service(
App::new()
.service(
web::scope("/foo")
.service(web::resource("/nested").name("nested").route(web::get().to(
|req: HttpRequest| {
HttpResponse::Ok()
.body(format!("{}", req.url_for_static("nested").unwrap()))
},
)))
.service(web::scope("/baz").service(web::resource("deep")))
.service(web::resource("{foo_param}")),
)
.service(web::scope("/bar").service(
web::resource("/nested").name("nested").route(web::get().to(
|req: HttpRequest| {
HttpResponse::Ok()
.body(format!("{}", req.url_for_static("nested").unwrap()))
},
)),
)),
)
.await;
let foo_resp =
test::call_service(&srv, TestRequest::with_uri("/foo/nested").to_request()).await;
assert_eq!(foo_resp.status(), StatusCode::OK);
let body = read_body(foo_resp).await;
// `body` equals http://localhost:8080/bar/nested
// because nested from /bar overrides /foo's
// to do this any other way would require something like a custom tree search
assert_eq!(body, "http://localhost:8080/bar/nested");
let bar_resp =
test::call_service(&srv, TestRequest::with_uri("/bar/nested").to_request()).await;
assert_eq!(bar_resp.status(), StatusCode::OK);
let body = read_body(bar_resp).await;
assert_eq!(body, "http://localhost:8080/bar/nested");
}
}

View File

@ -2,7 +2,10 @@ use std::{any::type_name, ops::Deref};
use actix_utils::future::{err, ok, Ready};
use crate::{dev::Payload, error::ErrorInternalServerError, Error, FromRequest, HttpRequest};
use crate::{
dev::Payload, error::ErrorInternalServerError, Error, FromRequest, HttpMessage as _,
HttpRequest,
};
/// Request-local data extractor.
///
@ -17,13 +20,13 @@ use crate::{dev::Payload, error::ErrorInternalServerError, Error, FromRequest, H
/// # Mutating Request Data
/// Note that since extractors must output owned data, only types that `impl Clone` can use this
/// extractor. A clone is taken of the required request data and can, therefore, not be directly
/// mutated in-place. To mutate request data, continue to use [`HttpRequest::req_data_mut`] or
/// mutated in-place. To mutate request data, continue to use [`HttpRequest::extensions_mut`] or
/// re-insert the cloned data back into the extensions map. A `DerefMut` impl is intentionally not
/// provided to make this potential foot-gun more obvious.
///
/// # Example
/// # Examples
/// ```no_run
/// # use actix_web::{web, HttpResponse, HttpRequest, Responder};
/// # use actix_web::{web, HttpResponse, HttpRequest, Responder, HttpMessage as _};
///
/// #[derive(Debug, Clone, PartialEq)]
/// struct FlagFromMiddleware(String);
@ -35,7 +38,7 @@ use crate::{dev::Payload, error::ErrorInternalServerError, Error, FromRequest, H
/// ) -> impl Responder {
/// // use an option extractor if middleware is not guaranteed to add this type of req data
/// if let Some(flag) = opt_flag {
/// assert_eq!(&flag.into_inner(), req.req_data().get::<FlagFromMiddleware>().unwrap());
/// assert_eq!(&flag.into_inner(), req.extensions().get::<FlagFromMiddleware>().unwrap());
/// }
///
/// HttpResponse::Ok()
@ -67,7 +70,7 @@ impl<T: Clone + 'static> FromRequest for ReqData<T> {
type Future = Ready<Result<Self, Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
if let Some(st) = req.req_data().get::<T>() {
if let Some(st) = req.extensions().get::<T>() {
ok(ReqData(st.clone()))
} else {
log::debug!(

View File

@ -1,6 +1,6 @@
use std::{cell::RefCell, fmt, future::Future, marker::PhantomData, rc::Rc};
use std::{cell::RefCell, fmt, future::Future, rc::Rc};
use actix_http::{body::BoxBody, Extensions};
use actix_http::Extensions;
use actix_router::{IntoPatterns, Patterns};
use actix_service::{
apply, apply_fn_factory, boxed, fn_service, IntoServiceFactory, Service, ServiceFactory,
@ -42,7 +42,7 @@ use crate::{
///
/// If no matching route could be found, *405* response code get returned. Default behavior could be
/// overridden with `default_resource()` method.
pub struct Resource<T = ResourceEndpoint, B = BoxBody> {
pub struct Resource<T = ResourceEndpoint> {
endpoint: T,
rdef: Patterns,
name: Option<String>,
@ -51,7 +51,6 @@ pub struct Resource<T = ResourceEndpoint, B = BoxBody> {
guards: Vec<Box<dyn Guard>>,
default: BoxedHttpServiceFactory,
factory_ref: Rc<RefCell<Option<ResourceFactory>>>,
_phantom: PhantomData<B>,
}
impl Resource {
@ -69,21 +68,13 @@ impl Resource {
default: boxed::factory(fn_service(|req: ServiceRequest| async {
Ok(req.into_response(HttpResponse::MethodNotAllowed()))
})),
_phantom: PhantomData,
}
}
}
impl<T, B> Resource<T, B>
impl<T> Resource<T>
where
T: ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
>,
B: MessageBody,
T: ServiceFactory<ServiceRequest, Config = (), Error = Error, InitError = ()>,
{
/// Set resource name.
///
@ -241,35 +232,35 @@ where
self
}
/// Register a resource middleware.
/// Registers a resource middleware.
///
/// This is similar to `App's` middlewares, but middleware get invoked on resource level.
/// Resource level middlewares are not allowed to change response
/// type (i.e modify response's body).
/// `mw` is a middleware component (type), that can modify the request and response across all
/// routes managed by this `Resource`.
///
/// **Note**: middlewares get called in opposite order of middlewares registration.
pub fn wrap<M, B1>(
/// See [`App::wrap`](crate::App::wrap) for more details.
#[doc(alias = "middleware")]
#[doc(alias = "use")] // nodejs terminology
pub fn wrap<M, B>(
self,
mw: M,
) -> Resource<
impl ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<B1>,
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
>,
B1,
>
where
M: Transform<
T::Service,
ServiceRequest,
Response = ServiceResponse<B1>,
Error = Error,
InitError = (),
>,
B1: MessageBody,
T::Service,
ServiceRequest,
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
> + 'static,
B: MessageBody,
{
Resource {
endpoint: apply(mw, self.endpoint),
@ -280,61 +271,34 @@ where
default: self.default,
app_data: self.app_data,
factory_ref: self.factory_ref,
_phantom: PhantomData,
}
}
/// Register a resource middleware function.
/// Registers a resource function middleware.
///
/// This function accepts instance of `ServiceRequest` type and
/// mutable reference to the next middleware in chain.
/// `mw` is a closure that runs during inbound and/or outbound processing in the request
/// life-cycle (request -> response), modifying request/response as necessary, across all
/// requests handled by the `Resource`.
///
/// This is similar to `App's` middlewares, but middleware get invoked on resource level.
/// Resource level middlewares are not allowed to change response
/// type (i.e modify response's body).
///
/// ```
/// use actix_service::Service;
/// use actix_web::{web, App};
/// use actix_web::http::header::{CONTENT_TYPE, HeaderValue};
///
/// async fn index() -> &'static str {
/// "Welcome!"
/// }
///
/// fn main() {
/// let app = App::new().service(
/// web::resource("/index.html")
/// .wrap_fn(|req, srv| {
/// let fut = srv.call(req);
/// async {
/// let mut res = fut.await?;
/// res.headers_mut().insert(
/// CONTENT_TYPE, HeaderValue::from_static("text/plain"),
/// );
/// Ok(res)
/// }
/// })
/// .route(web::get().to(index)));
/// }
/// ```
pub fn wrap_fn<F, R, B1>(
/// See [`App::wrap_fn`](crate::App::wrap_fn) for examples and more details.
#[doc(alias = "middleware")]
#[doc(alias = "use")] // nodejs terminology
pub fn wrap_fn<F, R, B>(
self,
mw: F,
) -> Resource<
impl ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<B1>,
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
>,
B1,
>
where
F: Fn(ServiceRequest, &T::Service) -> R + Clone,
R: Future<Output = Result<ServiceResponse<B1>, Error>>,
B1: MessageBody,
F: Fn(ServiceRequest, &T::Service) -> R + Clone + 'static,
R: Future<Output = Result<ServiceResponse<B>, Error>>,
B: MessageBody,
{
Resource {
endpoint: apply_fn_factory(self.endpoint, mw),
@ -345,7 +309,6 @@ where
default: self.default,
app_data: self.app_data,
factory_ref: self.factory_ref,
_phantom: PhantomData,
}
}
@ -373,7 +336,7 @@ where
}
}
impl<T, B> HttpServiceFactory for Resource<T, B>
impl<T, B> HttpServiceFactory for Resource<T>
where
T: ServiceFactory<
ServiceRequest,
@ -517,7 +480,7 @@ mod tests {
header::{self, HeaderValue},
Method, StatusCode,
},
middleware::{Compat, DefaultHeaders},
middleware::DefaultHeaders,
service::{ServiceRequest, ServiceResponse},
test::{call_service, init_service, TestRequest},
web, App, Error, HttpMessage, HttpResponse,
@ -525,31 +488,35 @@ mod tests {
#[test]
fn can_be_returned_from_fn() {
fn my_resource() -> Resource {
web::resource("/test").route(web::get().to(|| async { "hello" }))
fn my_resource_1() -> Resource {
web::resource("/test1").route(web::get().to(|| async { "hello" }))
}
fn my_compat_resource() -> Resource<
fn my_resource_2() -> Resource<
impl ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse,
Response = ServiceResponse<impl MessageBody>,
Error = Error,
InitError = (),
>,
> {
web::resource("/test-compat")
web::resource("/test2")
.wrap_fn(|req, srv| {
let fut = srv.call(req);
async { Ok(fut.await?.map_into_right_body::<()>()) }
})
.wrap(Compat::noop())
.route(web::get().to(|| async { "hello" }))
}
fn my_resource_3() -> impl HttpServiceFactory {
web::resource("/test3").route(web::get().to(|| async { "hello" }))
}
App::new()
.service(my_resource())
.service(my_compat_resource());
.service(my_resource_1())
.service(my_resource_2())
.service(my_resource_3());
}
#[actix_rt::test]

View File

@ -6,24 +6,18 @@ use std::{
task::{Context, Poll},
};
use actix_http::{
body::{BodyStream, BoxBody, MessageBody},
error::HttpError,
header::{self, HeaderName, TryIntoHeaderPair, TryIntoHeaderValue},
ConnectionType, Extensions, Response, ResponseHead, StatusCode,
};
use actix_http::{error::HttpError, Response, ResponseHead};
use bytes::Bytes;
use futures_core::Stream;
use serde::Serialize;
#[cfg(feature = "cookies")]
use actix_http::header::HeaderValue;
#[cfg(feature = "cookies")]
use cookie::{Cookie, CookieJar};
use crate::{
body::{BodyStream, BoxBody, MessageBody},
dev::Extensions,
error::{Error, JsonPayloadError},
BoxError, HttpResponse,
http::header::{self, HeaderName, TryIntoHeaderPair, TryIntoHeaderValue},
http::{ConnectionType, StatusCode},
BoxError, HttpRequest, HttpResponse, Responder,
};
/// An HTTP response builder.
@ -31,9 +25,7 @@ use crate::{
/// This type can be used to construct an instance of `Response` through a builder-like pattern.
pub struct HttpResponseBuilder {
res: Option<Response<BoxBody>>,
err: Option<HttpError>,
#[cfg(feature = "cookies")]
cookies: Option<CookieJar>,
error: Option<HttpError>,
}
impl HttpResponseBuilder {
@ -42,9 +34,7 @@ impl HttpResponseBuilder {
pub fn new(status: StatusCode) -> Self {
Self {
res: Some(Response::with_body(status, BoxBody::new(()))),
err: None,
#[cfg(feature = "cookies")]
cookies: None,
error: None,
}
}
@ -73,7 +63,7 @@ impl HttpResponseBuilder {
Ok((key, value)) => {
parts.headers.insert(key, value);
}
Err(e) => self.err = Some(e.into()),
Err(e) => self.error = Some(e.into()),
};
}
@ -95,7 +85,7 @@ impl HttpResponseBuilder {
if let Some(parts) = self.inner() {
match header.try_into_pair() {
Ok((key, value)) => parts.headers.append(key, value),
Err(e) => self.err = Some(e.into()),
Err(e) => self.error = Some(e.into()),
};
}
@ -114,14 +104,14 @@ impl HttpResponseBuilder {
K::Error: Into<HttpError>,
V: TryIntoHeaderValue,
{
if self.err.is_some() {
if self.error.is_some() {
return self;
}
match (key.try_into(), value.try_into_value()) {
(Ok(name), Ok(value)) => return self.insert_header((name, value)),
(Err(err), _) => self.err = Some(err.into()),
(_, Err(err)) => self.err = Some(err.into()),
(Err(err), _) => self.error = Some(err.into()),
(_, Err(err)) => self.error = Some(err.into()),
}
self
@ -139,14 +129,14 @@ impl HttpResponseBuilder {
K::Error: Into<HttpError>,
V: TryIntoHeaderValue,
{
if self.err.is_some() {
if self.error.is_some() {
return self;
}
match (key.try_into(), value.try_into_value()) {
(Ok(name), Ok(value)) => return self.append_header((name, value)),
(Err(err), _) => self.err = Some(err.into()),
(_, Err(err)) => self.err = Some(err.into()),
(Err(err), _) => self.error = Some(err.into()),
(_, Err(err)) => self.error = Some(err.into()),
}
self
@ -219,18 +209,23 @@ impl HttpResponseBuilder {
Ok(value) => {
parts.headers.insert(header::CONTENT_TYPE, value);
}
Err(e) => self.err = Some(e.into()),
Err(e) => self.error = Some(e.into()),
};
}
self
}
/// Set a cookie.
/// Add a cookie to the response.
///
/// To send a "removal" cookie, call [`.make_removal()`](cookie::Cookie::make_removal) on the
/// given cookie. See [`HttpResponse::add_removal_cookie()`] to learn more.
///
/// # Examples
/// Send a new cookie:
/// ```
/// use actix_web::{HttpResponse, cookie::Cookie};
///
/// HttpResponse::Ok()
/// let res = HttpResponse::Ok()
/// .cookie(
/// Cookie::build("name", "value")
/// .domain("www.rust-lang.org")
@ -241,48 +236,34 @@ impl HttpResponseBuilder {
/// )
/// .finish();
/// ```
#[cfg(feature = "cookies")]
pub fn cookie<'c>(&mut self, cookie: Cookie<'c>) -> &mut Self {
if self.cookies.is_none() {
let mut jar = CookieJar::new();
jar.add(cookie.into_owned());
self.cookies = Some(jar)
} else {
self.cookies.as_mut().unwrap().add(cookie.into_owned());
}
self
}
/// Remove cookie.
///
/// A `Set-Cookie` header is added that will delete a cookie with the same name from the client.
///
/// Send a removal cookie:
/// ```
/// use actix_web::{HttpRequest, HttpResponse, Responder};
/// use actix_web::{HttpResponse, cookie::Cookie};
///
/// async fn handler(req: HttpRequest) -> impl Responder {
/// let mut builder = HttpResponse::Ok();
/// // the name, domain and path match the cookie created in the previous example
/// let mut cookie = Cookie::build("name", "value-does-not-matter")
/// .domain("www.rust-lang.org")
/// .path("/")
/// .finish();
/// cookie.make_removal();
///
/// if let Some(ref cookie) = req.cookie("name") {
/// builder.del_cookie(cookie);
/// }
///
/// builder.finish()
/// }
/// let res = HttpResponse::Ok()
/// .cookie(cookie)
/// .finish();
/// ```
#[cfg(feature = "cookies")]
pub fn del_cookie(&mut self, cookie: &Cookie<'_>) -> &mut Self {
if self.cookies.is_none() {
self.cookies = Some(CookieJar::new())
pub fn cookie(&mut self, cookie: cookie::Cookie<'_>) -> &mut Self {
match cookie.to_string().try_into_value() {
Ok(hdr_val) => self.append_header((header::SET_COOKIE, hdr_val)),
Err(err) => {
self.error = Some(err.into());
self
}
}
let jar = self.cookies.as_mut().unwrap();
let cookie = cookie.clone().into_owned();
jar.add_original(cookie.clone());
jar.remove(cookie);
self
}
/// Responses extensions
/// Returns a reference to the response-local data/extensions container.
#[inline]
pub fn extensions(&self) -> Ref<'_, Extensions> {
self.res
@ -291,7 +272,8 @@ impl HttpResponseBuilder {
.extensions()
}
/// Mutable reference to a the response's extensions
/// Returns a mutable reference to the response-local data/extensions container.
#[inline]
pub fn extensions_mut(&mut self) -> RefMut<'_, Extensions> {
self.res
.as_mut()
@ -301,6 +283,9 @@ impl HttpResponseBuilder {
/// Set a body and build the `HttpResponse`.
///
/// Unlike [`message_body`](Self::message_body), errors are converted into error
/// responses immediately.
///
/// `HttpResponseBuilder` can not be used after this call.
pub fn body<B>(&mut self, body: B) -> HttpResponse<BoxBody>
where
@ -316,7 +301,7 @@ impl HttpResponseBuilder {
///
/// `HttpResponseBuilder` can not be used after this call.
pub fn message_body<B>(&mut self, body: B) -> Result<HttpResponse<B>, Error> {
if let Some(err) = self.err.take() {
if let Some(err) = self.error.take() {
return Err(err.into());
}
@ -326,20 +311,7 @@ impl HttpResponseBuilder {
.expect("cannot reuse response builder")
.set_body(body);
#[allow(unused_mut)] // mut is only unused when cookies are disabled
let mut res = HttpResponse::from(res);
#[cfg(feature = "cookies")]
if let Some(ref jar) = self.cookies {
for cookie in jar.delta() {
match HeaderValue::from_str(&cookie.to_string()) {
Ok(val) => res.headers_mut().append(header::SET_COOKIE, val),
Err(err) => return Err(err.into()),
};
}
}
Ok(res)
Ok(HttpResponse::from(res))
}
/// Set a streaming body and build the `HttpResponse`.
@ -388,15 +360,12 @@ impl HttpResponseBuilder {
pub fn take(&mut self) -> Self {
Self {
res: self.res.take(),
err: self.err.take(),
#[cfg(feature = "cookies")]
cookies: self.cookies.take(),
error: self.error.take(),
}
}
#[inline]
fn inner(&mut self) -> Option<&mut ResponseHead> {
if self.err.is_some() {
if self.error.is_some() {
return None;
}
@ -424,12 +393,20 @@ impl Future for HttpResponseBuilder {
}
}
impl Responder for HttpResponseBuilder {
type Body = BoxBody;
#[inline]
fn respond_to(mut self, _: &HttpRequest) -> HttpResponse<Self::Body> {
self.finish()
}
}
#[cfg(test)]
mod tests {
use actix_http::body;
use super::*;
use crate::{
body,
http::{
header::{self, HeaderValue, CONTENT_TYPE},
StatusCode,

View File

@ -1,12 +1,9 @@
use actix_http::{
body::{EitherBody, MessageBody},
error::HttpError,
header::HeaderMap,
header::TryIntoHeaderPair,
body::EitherBody, error::HttpError, header::HeaderMap, header::TryIntoHeaderPair,
StatusCode,
};
use crate::{BoxError, HttpRequest, HttpResponse, Responder};
use crate::{HttpRequest, HttpResponse, Responder};
/// Allows overriding status code and headers for a [`Responder`].
///
@ -143,7 +140,6 @@ impl<R: Responder> CustomizeResponder<R> {
impl<T> Responder for CustomizeResponder<T>
where
T: Responder,
<T::Body as MessageBody>::Error: Into<BoxError>,
{
type Body = EitherBody<T::Body>;

View File

@ -7,7 +7,7 @@ use actix_http::{
};
use bytes::{Bytes, BytesMut};
use crate::{BoxError, Error, HttpRequest, HttpResponse, HttpResponseBuilder};
use crate::{Error, HttpRequest, HttpResponse};
use super::CustomizeResponder;
@ -57,15 +57,6 @@ pub trait Responder {
}
}
impl Responder for HttpResponse {
type Body = BoxBody;
#[inline]
fn respond_to(self, _: &HttpRequest) -> HttpResponse<Self::Body> {
self
}
}
impl Responder for actix_http::Response<BoxBody> {
type Body = BoxBody;
@ -75,15 +66,6 @@ impl Responder for actix_http::Response<BoxBody> {
}
}
impl Responder for HttpResponseBuilder {
type Body = BoxBody;
#[inline]
fn respond_to(mut self, _: &HttpRequest) -> HttpResponse<Self::Body> {
self.finish()
}
}
impl Responder for actix_http::ResponseBuilder {
type Body = BoxBody;
@ -96,7 +78,6 @@ impl Responder for actix_http::ResponseBuilder {
impl<T> Responder for Option<T>
where
T: Responder,
<T::Body as MessageBody>::Error: Into<BoxError>,
{
type Body = EitherBody<T::Body>;
@ -111,7 +92,6 @@ where
impl<T, E> Responder for Result<T, E>
where
T: Responder,
<T::Body as MessageBody>::Error: Into<BoxError>,
E: Into<Error>,
{
type Body = EitherBody<T::Body>;

View File

@ -22,12 +22,12 @@ use {
cookie::Cookie,
};
use crate::{error::Error, HttpResponseBuilder};
use crate::{error::Error, HttpRequest, HttpResponseBuilder, Responder};
/// An outgoing response.
pub struct HttpResponse<B = BoxBody> {
res: Response<B>,
pub(crate) error: Option<Error>,
error: Option<Error>,
}
impl HttpResponse<BoxBody> {
@ -116,18 +116,54 @@ impl<B> HttpResponse<B> {
}
}
/// Add a cookie to this response
/// Add a cookie to this response.
///
/// # Errors
/// Returns an error if the cookie results in a malformed `Set-Cookie` header.
#[cfg(feature = "cookies")]
pub fn add_cookie(&mut self, cookie: &Cookie<'_>) -> Result<(), HttpError> {
HeaderValue::from_str(&cookie.to_string())
.map(|c| {
self.headers_mut().append(header::SET_COOKIE, c);
})
.map_err(|e| e.into())
.map(|cookie| self.headers_mut().append(header::SET_COOKIE, cookie))
.map_err(Into::into)
}
/// Remove all cookies with the given name from this response. Returns
/// the number of cookies removed.
/// Add a "removal" cookie to the response that matches attributes of given cookie.
///
/// This will cause browsers/clients to remove stored cookies with this name.
///
/// The `Set-Cookie` header added to the response will have:
/// - name matching given cookie;
/// - domain matching given cookie;
/// - path matching given cookie;
/// - an empty value;
/// - a max-age of `0`;
/// - an expiration date far in the past.
///
/// If the cookie you're trying to remove has an explicit path or domain set, those attributes
/// will need to be included in the cookie passed in here.
///
/// # Errors
/// Returns an error if the given name results in a malformed `Set-Cookie` header.
#[cfg(feature = "cookies")]
pub fn add_removal_cookie(&mut self, cookie: &Cookie<'_>) -> Result<(), HttpError> {
let mut removal_cookie = cookie.to_owned();
removal_cookie.make_removal();
HeaderValue::from_str(&removal_cookie.to_string())
.map(|cookie| self.headers_mut().append(header::SET_COOKIE, cookie))
.map_err(Into::into)
}
/// Remove all cookies with the given name from this response.
///
/// Returns the number of cookies removed.
///
/// This method can _not_ cause a browser/client to delete any of its stored cookies. Its only
/// purpose is to delete cookies that were added to this response using [`add_cookie`]
/// and [`add_removal_cookie`]. Use [`add_removal_cookie`] to send a "removal" cookie.
///
/// [`add_cookie`]: Self::add_cookie
/// [`add_removal_cookie`]: Self::add_removal_cookie
#[cfg(feature = "cookies")]
pub fn del_cookie(&mut self, name: &str) -> usize {
let headers = self.headers_mut();
@ -140,6 +176,7 @@ impl<B> HttpResponse<B> {
headers.remove(header::SET_COOKIE);
let mut count: usize = 0;
for v in vals {
if let Ok(s) = v.to_str() {
if let Ok(c) = Cookie::parse_encoded(s) {
@ -168,34 +205,37 @@ impl<B> HttpResponse<B> {
self.res.keep_alive()
}
/// Responses extensions
/// Returns reference to the response-local data/extensions container.
#[inline]
pub fn extensions(&self) -> Ref<'_, Extensions> {
self.res.extensions()
}
/// Mutable reference to a the response's extensions
/// Returns reference to the response-local data/extensions container.
#[inline]
pub fn extensions_mut(&mut self) -> RefMut<'_, Extensions> {
self.res.extensions_mut()
}
/// Get body of this response
/// Returns a reference to this response's body.
#[inline]
pub fn body(&self) -> &B {
self.res.body()
}
/// Set a body
/// Sets new body.
pub fn set_body<B2>(self, body: B2) -> HttpResponse<B2> {
HttpResponse {
res: self.res.set_body(body),
error: None,
// error: self.error, ??
error: self.error,
}
}
/// Split response and body
/// Returns split head and body.
///
/// # Implementation Notes
/// Due to internal performance optimizations, the first element of the returned tuple is an
/// `HttpResponse` as well but only contains the head of the response this was called on.
pub fn into_parts(self) -> (HttpResponse<()>, B) {
let (head, body) = self.res.into_parts();
@ -208,7 +248,7 @@ impl<B> HttpResponse<B> {
)
}
/// Drop request's body
/// Drops body and returns new response.
pub fn drop_body(self) -> HttpResponse<()> {
HttpResponse {
res: self.res.drop_body(),
@ -216,7 +256,9 @@ impl<B> HttpResponse<B> {
}
}
/// Set a body and return previous body value
/// Map the current body type to another using a closure. Returns a new response.
///
/// Closure receives the response head and the current body type.
pub fn map_body<F, B2>(self, f: F) -> HttpResponse<B2>
where
F: FnOnce(&mut ResponseHead, B) -> B2,
@ -311,6 +353,18 @@ impl Future for HttpResponse<BoxBody> {
}
}
impl<B> Responder for HttpResponse<B>
where
B: MessageBody + 'static,
{
type Body = B;
#[inline]
fn respond_to(self, _: &HttpRequest) -> HttpResponse<Self::Body> {
self
}
}
#[cfg(feature = "cookies")]
pub struct CookieIter<'a> {
iter: std::slice::Iter<'a, HeaderValue>,
@ -333,9 +387,16 @@ impl<'a> Iterator for CookieIter<'a> {
#[cfg(test)]
mod tests {
use static_assertions::assert_impl_all;
use super::*;
use crate::http::header::{HeaderValue, COOKIE};
assert_impl_all!(HttpResponse: Responder);
assert_impl_all!(HttpResponse<String>: Responder);
assert_impl_all!(HttpResponse<&'static str>: Responder);
assert_impl_all!(HttpResponse<crate::body::None>: Responder);
#[test]
fn test_debug() {
let resp = HttpResponse::Ok()
@ -346,3 +407,23 @@ mod tests {
assert!(dbg.contains("HttpResponse"));
}
}
#[cfg(test)]
#[cfg(feature = "cookies")]
mod cookie_tests {
use super::*;
#[test]
fn removal_cookies() {
let mut res = HttpResponse::Ok().finish();
let cookie = Cookie::new("foo", "");
res.add_removal_cookie(&cookie).unwrap();
let set_cookie_hdr = res.headers().get(header::SET_COOKIE).unwrap();
assert_eq!(
&set_cookie_hdr.as_bytes()[..25],
&b"foo=; Max-Age=0; Expires="[..],
"unexpected set-cookie value: {:?}",
set_cookie_hdr.to_str()
);
}
}

View File

@ -1,6 +1,7 @@
use std::{
borrow::Cow,
cell::RefCell,
fmt::Write as _,
rc::{Rc, Weak},
};
@ -10,12 +11,14 @@ use url::Url;
use crate::{error::UrlGenerationError, request::HttpRequest};
const AVG_PATH_LEN: usize = 24;
#[derive(Clone, Debug)]
pub struct ResourceMap {
pattern: ResourceDef,
/// Named resources within the tree or, for external resources,
/// it points to isolated nodes outside the tree.
/// Named resources within the tree or, for external resources, it points to isolated nodes
/// outside the tree.
named: AHashMap<String, Rc<ResourceMap>>,
parent: RefCell<Weak<ResourceMap>>,
@ -35,6 +38,35 @@ impl ResourceMap {
}
}
/// Format resource map as tree structure (unfinished).
#[allow(dead_code)]
pub(crate) fn tree(&self) -> String {
let mut buf = String::new();
self._tree(&mut buf, 0);
buf
}
pub(crate) fn _tree(&self, buf: &mut String, level: usize) {
if let Some(children) = &self.nodes {
for child in children {
writeln!(
buf,
"{}{} {}",
"--".repeat(level),
child.pattern.pattern().unwrap(),
child
.pattern
.name()
.map(|name| format!("({})", name))
.unwrap_or_else(|| "".to_owned())
)
.unwrap();
ResourceMap::_tree(child, buf, level + 1);
}
}
}
/// Adds a (possibly nested) resource.
///
/// To add a non-prefix pattern, `nested` must be `None`.
@ -44,7 +76,11 @@ impl ResourceMap {
pattern.set_id(self.nodes.as_ref().unwrap().len() as u16);
if let Some(new_node) = nested {
assert_eq!(&new_node.pattern, pattern, "`patern` and `nested` mismatch");
debug_assert_eq!(
&new_node.pattern, pattern,
"`pattern` and `nested` mismatch"
);
// parents absorb references to the named resources of children
self.named.extend(new_node.named.clone().into_iter());
self.nodes.as_mut().unwrap().push(new_node);
} else {
@ -64,7 +100,7 @@ impl ResourceMap {
None => false,
};
// Don't add external resources to the tree
// don't add external resources to the tree
if !is_external {
self.nodes.as_mut().unwrap().push(new_node);
}
@ -78,7 +114,7 @@ impl ResourceMap {
}
}
/// Generate url for named resource
/// Generate URL for named resource.
///
/// Check [`HttpRequest::url_for`] for detailed information.
pub fn url_for<U, I>(
@ -97,7 +133,7 @@ impl ResourceMap {
.named
.get(name)
.ok_or(UrlGenerationError::ResourceNotFound)?
.root_rmap_fn(String::with_capacity(24), |mut acc, node| {
.root_rmap_fn(String::with_capacity(AVG_PATH_LEN), |mut acc, node| {
node.pattern
.resource_path_from_iter(&mut acc, &mut elements)
.then(|| acc)
@ -128,6 +164,7 @@ impl ResourceMap {
Ok(url)
}
/// Returns true if there is a resource that would match `path`.
pub fn has_resource(&self, path: &str) -> bool {
self.find_matching_node(path).is_some()
}
@ -142,9 +179,10 @@ impl ResourceMap {
/// is possible.
pub fn match_pattern(&self, path: &str) -> Option<String> {
self.find_matching_node(path)?.root_rmap_fn(
String::with_capacity(24),
String::with_capacity(AVG_PATH_LEN),
|mut acc, node| {
acc.push_str(node.pattern.pattern()?);
let pattern = node.pattern.pattern()?;
acc.push_str(pattern);
Some(acc)
},
)
@ -490,4 +528,33 @@ mod tests {
"https://duck.com/abcd"
);
}
#[test]
fn url_for_override_within_map() {
let mut root = ResourceMap::new(ResourceDef::prefix(""));
let mut foo_rdef = ResourceDef::prefix("/foo");
let mut foo_map = ResourceMap::new(foo_rdef.clone());
let mut nested_rdef = ResourceDef::new("/nested");
nested_rdef.set_name("nested");
foo_map.add(&mut nested_rdef, None);
root.add(&mut foo_rdef, Some(Rc::new(foo_map)));
let mut foo_rdef = ResourceDef::prefix("/bar");
let mut foo_map = ResourceMap::new(foo_rdef.clone());
let mut nested_rdef = ResourceDef::new("/nested");
nested_rdef.set_name("nested");
foo_map.add(&mut nested_rdef, None);
root.add(&mut foo_rdef, Some(Rc::new(foo_map)));
let rmap = Rc::new(root);
ResourceMap::finish(&rmap);
let req = crate::test::TestRequest::default().to_http_request();
let url = rmap.url_for(&req, "nested", &[""; 0]).unwrap().to_string();
assert_eq!(url, "http://localhost:8080/bar/nested");
assert!(rmap.url_for(&req, "missing", &["u123"]).is_err());
}
}

View File

@ -1,9 +1,6 @@
use std::{cell::RefCell, fmt, future::Future, marker::PhantomData, mem, rc::Rc};
use std::{cell::RefCell, fmt, future::Future, mem, rc::Rc};
use actix_http::{
body::{BoxBody, MessageBody},
Extensions,
};
use actix_http::{body::MessageBody, Extensions};
use actix_router::{ResourceDef, Router};
use actix_service::{
apply, apply_fn_factory, boxed, IntoServiceFactory, Service, ServiceFactory,
@ -57,7 +54,7 @@ type Guards = Vec<Box<dyn Guard>>;
///
/// [pat]: crate::dev::ResourceDef#prefix-resources
/// [dynamic segments]: crate::dev::ResourceDef#dynamic-segments
pub struct Scope<T = ScopeEndpoint, B = BoxBody> {
pub struct Scope<T = ScopeEndpoint> {
endpoint: T,
rdef: String,
app_data: Option<Extensions>,
@ -66,7 +63,6 @@ pub struct Scope<T = ScopeEndpoint, B = BoxBody> {
default: Option<Rc<BoxedHttpServiceFactory>>,
external: Vec<ResourceDef>,
factory_ref: Rc<RefCell<Option<ScopeFactory>>>,
_phantom: PhantomData<B>,
}
impl Scope {
@ -83,21 +79,13 @@ impl Scope {
default: None,
external: Vec::new(),
factory_ref,
_phantom: Default::default(),
}
}
}
impl<T, B> Scope<T, B>
impl<T> Scope<T>
where
T: ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
>,
B: 'static,
T: ServiceFactory<ServiceRequest, Config = (), Error = Error, InitError = ()>,
{
/// Add match guard to a scope.
///
@ -296,32 +284,35 @@ where
self
}
/// Registers middleware, in the form of a middleware component (type), that runs during inbound
/// processing in the request life-cycle (request -> response), modifying request as necessary,
/// across all requests managed by the *Scope*.
/// Registers a scope-wide middleware.
///
/// Use middleware when you need to read or modify *every* request in some way.
pub fn wrap<M, B1>(
/// `mw` is a middleware component (type), that can modify the request and response across all
/// sub-resources managed by this `Scope`.
///
/// See [`App::wrap`](crate::App::wrap) for more details.
#[doc(alias = "middleware")]
#[doc(alias = "use")] // nodejs terminology
pub fn wrap<M, B>(
self,
mw: M,
) -> Scope<
impl ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<B1>,
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
>,
B1,
>
where
M: Transform<
T::Service,
ServiceRequest,
Response = ServiceResponse<B1>,
Error = Error,
InitError = (),
>,
T::Service,
ServiceRequest,
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
> + 'static,
B: MessageBody,
{
Scope {
endpoint: apply(mw, self.endpoint),
@ -332,54 +323,34 @@ where
default: self.default,
external: self.external,
factory_ref: self.factory_ref,
_phantom: PhantomData,
}
}
/// Registers middleware, in the form of a closure, that runs during inbound processing in the
/// request life-cycle (request -> response), modifying request as necessary, across all
/// requests managed by the *Scope*.
/// Registers a scope-wide function middleware.
///
/// # Examples
/// ```
/// use actix_service::Service;
/// use actix_web::{web, App};
/// use actix_web::http::header::{CONTENT_TYPE, HeaderValue};
/// `mw` is a closure that runs during inbound and/or outbound processing in the request
/// life-cycle (request -> response), modifying request/response as necessary, across all
/// requests handled by the `Scope`.
///
/// async fn index() -> &'static str {
/// "Welcome!"
/// }
///
/// let app = App::new().service(
/// web::scope("/app")
/// .wrap_fn(|req, srv| {
/// let fut = srv.call(req);
/// async {
/// let mut res = fut.await?;
/// res.headers_mut().insert(
/// CONTENT_TYPE, HeaderValue::from_static("text/plain"),
/// );
/// Ok(res)
/// }
/// })
/// .route("/index.html", web::get().to(index)));
/// ```
pub fn wrap_fn<F, R, B1>(
/// See [`App::wrap_fn`](crate::App::wrap_fn) for examples and more details.
#[doc(alias = "middleware")]
#[doc(alias = "use")] // nodejs terminology
pub fn wrap_fn<F, R, B>(
self,
mw: F,
) -> Scope<
impl ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<B1>,
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
>,
B1,
>
where
F: Fn(ServiceRequest, &T::Service) -> R + Clone,
R: Future<Output = Result<ServiceResponse<B1>, Error>>,
F: Fn(ServiceRequest, &T::Service) -> R + Clone + 'static,
R: Future<Output = Result<ServiceResponse<B>, Error>>,
B: MessageBody,
{
Scope {
endpoint: apply_fn_factory(self.endpoint, mw),
@ -390,12 +361,11 @@ where
default: self.default,
external: self.external,
factory_ref: self.factory_ref,
_phantom: PhantomData,
}
}
}
impl<T, B> HttpServiceFactory for Scope<T, B>
impl<T, B> HttpServiceFactory for Scope<T>
where
T: ServiceFactory<
ServiceRequest,
@ -596,7 +566,7 @@ mod tests {
header::{self, HeaderValue},
Method, StatusCode,
},
middleware::{Compat, DefaultHeaders},
middleware::DefaultHeaders,
service::{ServiceRequest, ServiceResponse},
test::{assert_body_eq, call_service, init_service, read_body, TestRequest},
web, App, HttpMessage, HttpRequest, HttpResponse,
@ -604,16 +574,16 @@ mod tests {
#[test]
fn can_be_returned_from_fn() {
fn my_scope() -> Scope {
fn my_scope_1() -> Scope {
web::scope("/test")
.service(web::resource("").route(web::get().to(|| async { "hello" })))
}
fn my_compat_scope() -> Scope<
fn my_scope_2() -> Scope<
impl ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse,
Response = ServiceResponse<impl MessageBody>,
Error = Error,
InitError = (),
>,
@ -623,11 +593,17 @@ mod tests {
let fut = srv.call(req);
async { Ok(fut.await?.map_into_right_body::<()>()) }
})
.wrap(Compat::noop())
.service(web::resource("").route(web::get().to(|| async { "hello" })))
}
App::new().service(my_scope()).service(my_compat_scope());
fn my_scope_3() -> impl HttpServiceFactory {
my_scope_2()
}
App::new()
.service(my_scope_1())
.service(my_scope_2())
.service(my_scope_3());
}
#[actix_rt::test]

View File

@ -97,12 +97,18 @@ impl ServiceRequest {
/// Construct request from parts.
pub fn from_parts(req: HttpRequest, payload: Payload) -> Self {
#[cfg(debug_assertions)]
if Rc::strong_count(&req.inner) > 1 {
log::warn!("Cloning an `HttpRequest` might cause panics.");
}
Self { req, payload }
}
/// Construct request from request.
///
/// The returned `ServiceRequest` would have no payload.
#[inline]
pub fn from_request(req: HttpRequest) -> Self {
ServiceRequest {
req,
@ -197,9 +203,9 @@ impl ServiceRequest {
self.req.connection_info()
}
/// Get a reference to the Path parameters.
/// Returns a reference to the Path parameters.
///
/// Params is a container for url parameters.
/// Params is a container for URL parameters.
/// A variable segment is specified in the form `{identifier}`,
/// where the identifier can be used later in a request handler to
/// access the matched value for that segment.
@ -208,6 +214,12 @@ impl ServiceRequest {
self.req.match_info()
}
/// Returns a mutable reference to the Path parameters.
#[inline]
pub fn match_info_mut(&mut self) -> &mut Path<Url> {
self.req.match_info_mut()
}
/// Counterpart to [`HttpRequest::match_name`].
#[inline]
pub fn match_name(&self) -> Option<&str> {
@ -220,12 +232,6 @@ impl ServiceRequest {
self.req.match_pattern()
}
/// Get a mutable reference to the Path parameters.
#[inline]
pub fn match_info_mut(&mut self) -> &mut Path<Url> {
self.req.match_info_mut()
}
/// Get a reference to a `ResourceMap` of current application.
#[inline]
pub fn resource_map(&self) -> &ResourceMap {
@ -256,18 +262,6 @@ impl ServiceRequest {
self.req.conn_data()
}
/// Counterpart to [`HttpRequest::req_data`].
#[inline]
pub fn req_data(&self) -> Ref<'_, Extensions> {
self.req.req_data()
}
/// Counterpart to [`HttpRequest::req_data_mut`].
#[inline]
pub fn req_data_mut(&self) -> RefMut<'_, Extensions> {
self.req.req_data_mut()
}
#[cfg(feature = "cookies")]
#[inline]
pub fn cookies(&self) -> Result<Ref<'_, Vec<Cookie<'static>>>, CookieParseError> {
@ -307,9 +301,11 @@ impl ServiceRequest {
}
}
impl Resource<Url> for ServiceRequest {
impl Resource for ServiceRequest {
type Path = Url;
#[inline]
fn resource_path(&mut self) -> &mut Path<Url> {
fn resource_path(&mut self) -> &mut Path<Self::Path> {
self.match_info_mut()
}
}
@ -318,18 +314,15 @@ impl HttpMessage for ServiceRequest {
type Stream = BoxedPayloadStream;
#[inline]
/// Returns Request's headers.
fn headers(&self) -> &HeaderMap {
&self.head().headers
}
/// Request extensions
#[inline]
fn extensions(&self) -> Ref<'_, Extensions> {
self.req.extensions()
}
/// Mutable reference to a the request's extensions
#[inline]
fn extensions_mut(&self) -> RefMut<'_, Extensions> {
self.req.extensions_mut()
@ -396,32 +389,32 @@ impl<B> ServiceResponse<B> {
ServiceResponse::new(self.request, response)
}
/// Get reference to original request
/// Returns reference to original request.
#[inline]
pub fn request(&self) -> &HttpRequest {
&self.request
}
/// Get reference to response
/// Returns reference to response.
#[inline]
pub fn response(&self) -> &HttpResponse<B> {
&self.response
}
/// Get mutable reference to response
/// Returns mutable reference to response.
#[inline]
pub fn response_mut(&mut self) -> &mut HttpResponse<B> {
&mut self.response
}
/// Get the response status code
/// Returns response status code.
#[inline]
pub fn status(&self) -> StatusCode {
self.response.status()
}
#[inline]
/// Returns response's headers.
#[inline]
pub fn headers(&self) -> &HeaderMap {
self.response.headers()
}
@ -438,13 +431,9 @@ impl<B> ServiceResponse<B> {
(self.request, self.response)
}
/// Extract response body
#[inline]
pub fn into_body(self) -> B {
self.response.into_body()
}
/// Set a new body
/// Map the current body type to another using a closure. Returns a new response.
///
/// Closure receives the response head and the current body type.
#[inline]
pub fn map_body<F, B2>(self, f: F) -> ServiceResponse<B2>
where
@ -475,6 +464,12 @@ impl<B> ServiceResponse<B> {
{
self.map_body(|_, body| body.boxed())
}
/// Consumes the response and returns its body.
#[inline]
pub fn into_body(self) -> B {
self.response.into_body()
}
}
impl<B> From<ServiceResponse<B>> for HttpResponse<B> {
@ -544,14 +539,12 @@ impl WebService {
/// Ok(req.into_response(HttpResponse::Ok().finish()))
/// }
///
/// fn main() {
/// let app = App::new()
/// .service(
/// web::service("/app")
/// .guard(guard::Header("content-type", "text/plain"))
/// .finish(index)
/// );
/// }
/// let app = App::new()
/// .service(
/// web::service("/app")
/// .guard(guard::Header("content-type", "text/plain"))
/// .finish(index)
/// );
/// ```
pub fn guard<G: Guard + 'static>(mut self, guard: G) -> Self {
self.guards.push(Box::new(guard));
@ -675,7 +668,7 @@ service_tuple! { A B C D E F G H I J K L }
#[cfg(test)]
mod tests {
use super::*;
use crate::test::{init_service, TestRequest};
use crate::test::{self, init_service, TestRequest};
use crate::{guard, http, web, App, HttpResponse};
use actix_service::Service;
use actix_utils::future::ok;
@ -822,4 +815,30 @@ mod tests {
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK);
}
#[actix_rt::test]
#[should_panic(expected = "called `Option::unwrap()` on a `None` value")]
async fn cloning_request_panics() {
async fn index(_name: web::Path<(String,)>) -> &'static str {
""
}
let app = test::init_service(
App::new()
.wrap_fn(|req, svc| {
let (req, pl) = req.into_parts();
let _req2 = req.clone();
let req = ServiceRequest::from_parts(req, pl);
svc.call(req)
})
.route("/", web::get().to(|| async { "" }))
.service(
web::resource("/resource1/{name}/index.html").route(web::get().to(index)),
),
)
.await;
let req = test::TestRequest::default().to_request();
let _res = test::call_service(&app, req).await;
}
}

View File

@ -1,4 +1,4 @@
use std::fmt;
use std::error::Error as StdError;
use actix_http::Request;
use actix_service::IntoServiceFactory;
@ -135,7 +135,6 @@ pub async fn call_and_read_body<S, B>(app: &S, req: Request) -> Bytes
where
S: Service<Request, Response = ServiceResponse<B>, Error = Error>,
B: MessageBody,
B::Error: fmt::Debug,
{
let res = call_service(app, req).await;
read_body(res).await
@ -147,7 +146,6 @@ pub async fn read_response<S, B>(app: &S, req: Request) -> Bytes
where
S: Service<Request, Response = ServiceResponse<B>, Error = Error>,
B: MessageBody,
B::Error: fmt::Debug,
{
let res = call_service(app, req).await;
read_body(res).await
@ -186,11 +184,11 @@ where
pub async fn read_body<B>(res: ServiceResponse<B>) -> Bytes
where
B: MessageBody,
B::Error: fmt::Debug,
{
let body = res.into_body();
body::to_bytes(body)
.await
.map_err(Into::<Box<dyn StdError>>::into)
.expect("error reading test response body")
}
@ -240,7 +238,6 @@ where
pub async fn read_body_json<T, B>(res: ServiceResponse<B>) -> T
where
B: MessageBody,
B::Error: fmt::Debug,
T: DeserializeOwned,
{
let body = read_body(res).await;
@ -300,7 +297,6 @@ pub async fn call_and_read_body_json<S, B, T>(app: &S, req: Request) -> T
where
S: Service<Request, Response = ServiceResponse<B>, Error = Error>,
B: MessageBody,
B::Error: fmt::Debug,
T: DeserializeOwned,
{
let res = call_service(app, req).await;
@ -313,7 +309,6 @@ pub async fn read_response_json<S, B, T>(app: &S, req: Request) -> T
where
S: Service<Request, Response = ServiceResponse<B>, Error = Error>,
B: MessageBody,
B::Error: fmt::Debug,
T: DeserializeOwned,
{
call_and_read_body_json(app, req).await
@ -325,7 +320,10 @@ mod tests {
use serde::{Deserialize, Serialize};
use super::*;
use crate::{http::header, test::TestRequest, web, App, HttpMessage, HttpResponse};
use crate::{
dev::ServiceRequest, http::header, test::TestRequest, web, App, HttpMessage,
HttpResponse,
};
#[actix_rt::test]
async fn test_request_methods() {
@ -471,4 +469,37 @@ mod tests {
assert_eq!(&result.id, "12345");
assert_eq!(&result.name, "User name");
}
#[actix_rt::test]
#[allow(dead_code)]
async fn return_opaque_types() {
fn test_app() -> App<
impl ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<impl MessageBody>,
Error = crate::Error,
InitError = (),
>,
> {
App::new().service(web::resource("/people").route(
web::post().to(|person: web::Json<Person>| HttpResponse::Ok().json(person)),
))
}
async fn test_service(
) -> impl Service<Request, Response = ServiceResponse<impl MessageBody>, Error = crate::Error>
{
init_service(test_app()).await
}
async fn compile_test(mut req: Vec<Request>) {
let svc = test_service().await;
call_service(&svc, req.pop().unwrap()).await;
call_and_read_body(&svc, req.pop().unwrap()).await;
read_body(call_service(&svc, req.pop().unwrap()).await).await;
let _: String = call_and_read_body_json(&svc, req.pop().unwrap()).await;
let _: String = read_body_json(call_service(&svc, req.pop().unwrap()).await).await;
}
}
}

View File

@ -11,7 +11,7 @@ use std::{
};
use actix_web::{
cookie::{Cookie, CookieBuilder},
cookie::Cookie,
http::{header, StatusCode},
middleware::{Compress, NormalizePath, TrailingSlash},
web, App, Error, HttpResponse,
@ -773,7 +773,7 @@ async fn test_server_cookies() {
App::new().default_service(web::to(|| {
HttpResponse::Ok()
.cookie(
CookieBuilder::new("first", "first_value")
Cookie::build("first", "first_value")
.http_only(true)
.finish(),
)
@ -787,13 +787,13 @@ async fn test_server_cookies() {
let res = req.send().await.unwrap();
assert!(res.status().is_success());
let first_cookie = CookieBuilder::new("first", "first_value")
let first_cookie = Cookie::build("first", "first_value")
.http_only(true)
.finish();
let second_cookie = Cookie::new("second", "second_value");
let second_cookie = Cookie::new("second", "first_value");
let cookies = res.cookies().expect("To have cookies");
assert_eq!(cookies.len(), 2);
assert_eq!(cookies.len(), 3);
if cookies[0] == first_cookie {
assert_eq!(cookies[1], second_cookie);
} else {
@ -809,7 +809,7 @@ async fn test_server_cookies() {
.get_all(http::header::SET_COOKIE)
.map(|header| header.to_str().expect("To str").to_string())
.collect::<Vec<_>>();
assert_eq!(cookies.len(), 2);
assert_eq!(cookies.len(), 3);
if cookies[0] == first_cookie {
assert_eq!(cookies[1], second_cookie);
} else {

View File

@ -41,16 +41,22 @@ pub mod deflate {
pub mod brotli {
use super::*;
use ::brotli2::{read::BrotliDecoder, write::BrotliEncoder};
use ::brotli::{reader::Decompressor as BrotliDecoder, CompressorWriter as BrotliEncoder};
pub fn encode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
let mut encoder = BrotliEncoder::new(Vec::new(), 3);
let mut encoder = BrotliEncoder::new(
Vec::new(),
8 * 1024, // 32 KiB buffer
3, // BROTLI_PARAM_QUALITY
22, // BROTLI_PARAM_LGWIN
);
encoder.write_all(bytes.as_ref()).unwrap();
encoder.finish().unwrap()
encoder.flush().unwrap();
encoder.into_inner()
}
pub fn decode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
let mut decoder = BrotliDecoder::new(bytes.as_ref());
let mut decoder = BrotliDecoder::new(bytes.as_ref(), 8_096);
let mut buf = Vec::new();
decoder.read_to_end(&mut buf).unwrap();
buf