mirror of https://github.com/fafhrd91/actix-web
Merge branch 'master' of github.com:actix/actix-web
This commit is contained in:
commit
5bd43783c6
|
@ -30,7 +30,7 @@ jobs:
|
|||
run: echo "<meta http-equiv=refresh content=0;url=os_balloon/index.html>" > target/doc/index.html
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: JamesIves/github-pages-deploy-action@3.5.8
|
||||
uses: JamesIves/github-pages-deploy-action@3.7.1
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BRANCH: gh-pages
|
||||
|
|
31
CHANGES.md
31
CHANGES.md
|
@ -1,8 +1,37 @@
|
|||
# Changes
|
||||
|
||||
## Unreleased - 2020-xx-xx
|
||||
### Fixed
|
||||
* added the actual parsing error to `test::read_body_json` [#1812]
|
||||
|
||||
[#1812]: https://github.com/actix/actix-web/pull/1812
|
||||
|
||||
|
||||
|
||||
## 3.3.2 - 2020-12-01
|
||||
### Fixed
|
||||
* Removed an occasional `unwrap` on `None` panic in `NormalizePathNormalization`. [#1762]
|
||||
* Fix `match_pattern()` returning `None` for scope with empty path resource. [#1798]
|
||||
* Increase minimum `socket2` version. [#1803]
|
||||
|
||||
[#1762]: https://github.com/actix/actix-web/pull/1762
|
||||
[#1798]: https://github.com/actix/actix-web/pull/1798
|
||||
[#1803]: https://github.com/actix/actix-web/pull/1803
|
||||
|
||||
|
||||
## 3.3.1 - 2020-11-29
|
||||
* Ensure `actix-http` dependency uses same `serde_urlencoded`.
|
||||
|
||||
|
||||
## 3.3.0 - 2020-11-25
|
||||
### Added
|
||||
* Add `Either<A, B>` extractor helper. [#1788]
|
||||
|
||||
### Changed
|
||||
* Upgrade `serde_urlencoded` to `0.7`.
|
||||
* Upgrade `serde_urlencoded` to `0.7`. [#1773]
|
||||
|
||||
[#1773]: https://github.com/actix/actix-web/pull/1773
|
||||
[#1788]: https://github.com/actix/actix-web/pull/1788
|
||||
|
||||
|
||||
## 3.2.0 - 2020-10-30
|
||||
|
|
18
Cargo.toml
18
Cargo.toml
|
@ -1,8 +1,8 @@
|
|||
[package]
|
||||
name = "actix-web"
|
||||
version = "3.2.0"
|
||||
version = "3.3.2"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Actix web is a powerful, pragmatic, and extremely fast web framework for Rust"
|
||||
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
|
||||
readme = "README.md"
|
||||
keywords = ["actix", "http", "web", "framework", "async"]
|
||||
homepage = "https://actix.rs"
|
||||
|
@ -34,7 +34,7 @@ members = [
|
|||
"actix-multipart",
|
||||
"actix-web-actors",
|
||||
"actix-web-codegen",
|
||||
"test-server",
|
||||
"actix-http-test",
|
||||
]
|
||||
|
||||
[features]
|
||||
|
@ -85,11 +85,11 @@ actix-threadpool = "0.3.1"
|
|||
actix-tls = "2.0.0"
|
||||
|
||||
actix-web-codegen = "0.4.0"
|
||||
actix-http = "2.1.0"
|
||||
awc = { version = "2.0.0", default-features = false }
|
||||
actix-http = "2.2.0"
|
||||
awc = { version = "2.0.3", default-features = false }
|
||||
|
||||
bytes = "0.5.3"
|
||||
derive_more = "0.99.2"
|
||||
derive_more = "0.99.5"
|
||||
encoding_rs = "0.8"
|
||||
futures-channel = { version = "0.3.5", default-features = false }
|
||||
futures-core = { version = "0.3.5", default-features = false }
|
||||
|
@ -97,7 +97,7 @@ futures-util = { version = "0.3.5", default-features = false }
|
|||
fxhash = "0.2.1"
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
socket2 = "0.3"
|
||||
socket2 = "0.3.16"
|
||||
pin-project = "1.0.0"
|
||||
regex = "1.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
@ -127,10 +127,10 @@ codegen-units = 1
|
|||
[patch.crates-io]
|
||||
actix-web = { path = "." }
|
||||
actix-http = { path = "actix-http" }
|
||||
actix-http-test = { path = "test-server" }
|
||||
actix-http-test = { path = "actix-http-test" }
|
||||
actix-web-codegen = { path = "actix-web-codegen" }
|
||||
actix-files = { path = "actix-files" }
|
||||
actix-multipart = { path = "actix-multipart" }
|
||||
actix-files = { path = "actix-files" }
|
||||
awc = { path = "awc" }
|
||||
|
||||
[[bench]]
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
<div align="center">
|
||||
<h1>Actix web</h1>
|
||||
<p>
|
||||
<strong>Actix web is a powerful, pragmatic, and extremely fast web framework for Rust</strong>
|
||||
<strong>Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust</strong>
|
||||
</p>
|
||||
<p>
|
||||
|
||||
[](https://crates.io/crates/actix-web)
|
||||
[](https://docs.rs/actix-web/3.2.0)
|
||||
[](https://docs.rs/actix-web/3.3.2)
|
||||
[](https://blog.rust-lang.org/2020/03/12/Rust-1.42.html)
|
||||

|
||||
[](https://deps.rs/crate/actix-web/3.2.0)
|
||||
[](https://deps.rs/crate/actix-web/3.3.2)
|
||||
<br />
|
||||
[](https://travis-ci.org/actix/actix-web)
|
||||
[](https://codecov.io/gh/actix/actix-web)
|
||||
|
|
|
@ -3,6 +3,10 @@
|
|||
## Unreleased - 2020-xx-xx
|
||||
|
||||
|
||||
## 0.4.1 - 2020-11-24
|
||||
* Clarify order of parameters in `Files::new` and improve docs.
|
||||
|
||||
|
||||
## 0.4.0 - 2020-10-06
|
||||
* Add `Files::prefer_utf8` option that adds UTF-8 charset on certain response types. [#1714]
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "actix-files"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Static file serving for Actix Web"
|
||||
readme = "README.md"
|
||||
|
@ -21,14 +21,14 @@ actix-web = { version = "3.0.0", default-features = false }
|
|||
actix-service = "1.0.6"
|
||||
bitflags = "1"
|
||||
bytes = "0.5.3"
|
||||
futures-core = { version = "0.3.5", default-features = false }
|
||||
futures-util = { version = "0.3.5", default-features = false }
|
||||
futures-core = { version = "0.3.7", default-features = false }
|
||||
futures-util = { version = "0.3.7", default-features = false }
|
||||
derive_more = "0.99.2"
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
mime_guess = "2.0.1"
|
||||
percent-encoding = "2.1"
|
||||
v_htmlescape = "0.10"
|
||||
v_htmlescape = "0.11"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "1.0.0"
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
> Static file serving for Actix Web
|
||||
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://docs.rs/actix-files)
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://docs.rs/actix-files/0.4.1)
|
||||
[](https://blog.rust-lang.org/2020/03/12/Rust-1.42.html)
|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-files/0.4.0)
|
||||
[](https://deps.rs/crate/actix-files/0.4.1)
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
|
|
|
@ -65,13 +65,25 @@ impl Clone for Files {
|
|||
}
|
||||
|
||||
impl Files {
|
||||
/// Create new `Files` instance for specified base directory.
|
||||
/// Create new `Files` instance for a specified base directory.
|
||||
///
|
||||
/// `File` uses `ThreadPool` for blocking filesystem operations.
|
||||
/// By default pool with 5x threads of available cpus is used.
|
||||
/// Pool size can be changed by setting ACTIX_THREADPOOL environment variable.
|
||||
pub fn new<T: Into<PathBuf>>(path: &str, dir: T) -> Files {
|
||||
let orig_dir = dir.into();
|
||||
/// # Argument Order
|
||||
/// The first argument (`mount_path`) is the root URL at which the static files are served.
|
||||
/// For example, `/assets` will serve files at `example.com/assets/...`.
|
||||
///
|
||||
/// The second argument (`serve_from`) is the location on disk at which files are loaded.
|
||||
/// This can be a relative path. For example, `./` would serve files from the current
|
||||
/// working directory.
|
||||
///
|
||||
/// # Implementation Notes
|
||||
/// If the mount path is set as the root path `/`, services registered after this one will
|
||||
/// be inaccessible. Register more specific handlers and services first.
|
||||
///
|
||||
/// `Files` uses a threadpool for blocking filesystem operations. By default, the pool uses a
|
||||
/// number of threads equal to 5x the number of available logical CPUs. Pool size can be changed
|
||||
/// by setting ACTIX_THREADPOOL environment variable.
|
||||
pub fn new<T: Into<PathBuf>>(mount_path: &str, serve_from: T) -> Files {
|
||||
let orig_dir = serve_from.into();
|
||||
let dir = match orig_dir.canonicalize() {
|
||||
Ok(canon_dir) => canon_dir,
|
||||
Err(_) => {
|
||||
|
@ -81,7 +93,7 @@ impl Files {
|
|||
};
|
||||
|
||||
Files {
|
||||
path: path.to_string(),
|
||||
path: mount_path.to_owned(),
|
||||
directory: dir,
|
||||
index: None,
|
||||
show_index: false,
|
||||
|
|
|
@ -2,15 +2,20 @@
|
|||
|
||||
## Unreleased - 2020-xx-xx
|
||||
|
||||
* add ability to set address for `TestServer` [#1645]
|
||||
* Upgrade `base64` to `0.13`.
|
||||
* Upgrade `serde_urlencoded` to `0.7`.
|
||||
|
||||
## 2.1.0 - 2020-11-25
|
||||
* Add ability to set address for `TestServer`. [#1645]
|
||||
* Upgrade `base64` to `0.13`.
|
||||
* Upgrade `serde_urlencoded` to `0.7`. [#1773]
|
||||
|
||||
[#1773]: https://github.com/actix/actix-web/pull/1773
|
||||
[#1645]: https://github.com/actix/actix-web/pull/1645
|
||||
|
||||
|
||||
## 2.0.0 - 2020-09-11
|
||||
* Update actix-codec and actix-utils dependencies.
|
||||
|
||||
|
||||
## 2.0.0-alpha.1 - 2020-05-23
|
||||
* Update the `time` dependency to 0.2.7
|
||||
* Update `actix-connect` dependency to 2.0.0-alpha.2
|
||||
|
@ -20,74 +25,56 @@
|
|||
* Update `base64` dependency to 0.12
|
||||
* Update `env_logger` dependency to 0.7
|
||||
|
||||
## [1.0.0] - 2019-12-13
|
||||
|
||||
### Changed
|
||||
|
||||
## 1.0.0 - 2019-12-13
|
||||
* Replaced `TestServer::start()` with `test_server()`
|
||||
|
||||
|
||||
## [1.0.0-alpha.3] - 2019-12-07
|
||||
|
||||
### Changed
|
||||
|
||||
## 1.0.0-alpha.3 - 2019-12-07
|
||||
* Migrate to `std::future`
|
||||
|
||||
|
||||
## [0.2.5] - 2019-09-17
|
||||
|
||||
### Changed
|
||||
|
||||
## 0.2.5 - 2019-09-17
|
||||
* Update serde_urlencoded to "0.6.1"
|
||||
* Increase TestServerRuntime timeouts from 500ms to 3000ms
|
||||
|
||||
### Fixed
|
||||
|
||||
* Do not override current `System`
|
||||
|
||||
|
||||
## [0.2.4] - 2019-07-18
|
||||
|
||||
## 0.2.4 - 2019-07-18
|
||||
* Update actix-server to 0.6
|
||||
|
||||
## [0.2.3] - 2019-07-16
|
||||
|
||||
## 0.2.3 - 2019-07-16
|
||||
* Add `delete`, `options`, `patch` methods to `TestServerRunner`
|
||||
|
||||
## [0.2.2] - 2019-06-16
|
||||
|
||||
## 0.2.2 - 2019-06-16
|
||||
* Add .put() and .sput() methods
|
||||
|
||||
## [0.2.1] - 2019-06-05
|
||||
|
||||
## 0.2.1 - 2019-06-05
|
||||
* Add license files
|
||||
|
||||
## [0.2.0] - 2019-05-12
|
||||
|
||||
## 0.2.0 - 2019-05-12
|
||||
* Update awc and actix-http deps
|
||||
|
||||
## [0.1.1] - 2019-04-24
|
||||
|
||||
## 0.1.1 - 2019-04-24
|
||||
* Always make new connection for http client
|
||||
|
||||
|
||||
## [0.1.0] - 2019-04-16
|
||||
|
||||
## 0.1.0 - 2019-04-16
|
||||
* No changes
|
||||
|
||||
|
||||
## [0.1.0-alpha.3] - 2019-04-02
|
||||
|
||||
## 0.1.0-alpha.3 - 2019-04-02
|
||||
* Request functions accept path #743
|
||||
|
||||
|
||||
## [0.1.0-alpha.2] - 2019-03-29
|
||||
|
||||
## 0.1.0-alpha.2 - 2019-03-29
|
||||
* Added TestServerRuntime::load_body() method
|
||||
|
||||
* Update actix-http and awc libraries
|
||||
|
||||
|
||||
## [0.1.0-alpha.1] - 2019-03-28
|
||||
|
||||
## 0.1.0-alpha.1 - 2019-03-28
|
||||
* Initial impl
|
|
@ -1,8 +1,8 @@
|
|||
[package]
|
||||
name = "actix-http-test"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Actix HTTP test server"
|
||||
description = "Various helpers for Actix applications to use during testing"
|
||||
readme = "README.md"
|
||||
keywords = ["http", "web", "framework", "async", "futures"]
|
||||
homepage = "https://actix.rs"
|
|
@ -0,0 +1,15 @@
|
|||
# actix-http-test
|
||||
|
||||
> Various helpers for Actix applications to use during testing.
|
||||
|
||||
[](https://crates.io/crates/actix-http-test)
|
||||
[](https://docs.rs/actix-http-test/2.1.0)
|
||||

|
||||
[](https://deps.rs/crate/actix-http-test/2.1.0)
|
||||
[](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
## Documentation & Resources
|
||||
|
||||
- [API Documentation](https://docs.rs/actix-http-test)
|
||||
- [Chat on Gitter](https://gitter.im/actix/actix-web)
|
||||
- Minimum Supported Rust Version (MSRV): 1.42.0
|
|
@ -1,4 +1,9 @@
|
|||
//! Various helpers for Actix applications to use during testing.
|
||||
|
||||
#![deny(rust_2018_idioms)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
|
||||
use std::sync::mpsc;
|
||||
use std::{net, thread, time};
|
||||
|
|
@ -1,17 +1,25 @@
|
|||
# Changes
|
||||
|
||||
## Unreleased - 2020-xx-xx
|
||||
|
||||
|
||||
## 2.2.0 - 2020-11-25
|
||||
### Added
|
||||
* HttpResponse builders for 1xx status codes. [#1768]
|
||||
* `Accept::mime_precedence` and `Accept::mime_preference`. [#1793]
|
||||
* `TryFrom<u16>` and `TryFrom<f32>` for `http::header::Quality`. [#1797]
|
||||
|
||||
### Fixed
|
||||
* Started dropping `transfer-encoding: chunked` and `Content-Length` for 1XX and 204 responses. [#1767]
|
||||
|
||||
### Changed
|
||||
* Upgrade `serde_urlencoded` to `0.7`.
|
||||
* Upgrade `serde_urlencoded` to `0.7`. [#1773]
|
||||
|
||||
[#1773]: https://github.com/actix/actix-web/pull/1773
|
||||
[#1767]: https://github.com/actix/actix-web/pull/1767
|
||||
[#1768]: https://github.com/actix/actix-web/pull/1768
|
||||
[#1793]: https://github.com/actix/actix-web/pull/1793
|
||||
[#1797]: https://github.com/actix/actix-web/pull/1797
|
||||
|
||||
|
||||
## 2.1.0 - 2020-10-30
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "actix-http"
|
||||
version = "2.1.0"
|
||||
version = "2.2.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "HTTP primitives for the Actix ecosystem"
|
||||
readme = "README.md"
|
||||
|
|
|
@ -3,14 +3,14 @@
|
|||
> HTTP primitives for the Actix ecosystem.
|
||||
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://docs.rs/actix-http/2.1.0)
|
||||
[](https://docs.rs/actix-http/2.2.0)
|
||||

|
||||
[](https://deps.rs/crate/actix-http/2.1.0)
|
||||
[](https://deps.rs/crate/actix-http/2.2.0)
|
||||
[](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
## Documentation & Resources
|
||||
|
||||
- [API Documentation](https://docs.rs/actix-http/2.1.0)
|
||||
- [API Documentation](https://docs.rs/actix-http)
|
||||
- [Chat on Gitter](https://gitter.im/actix/actix-web)
|
||||
- Minimum Supported Rust Version (MSRV): 1.42.0
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use std::cmp::Ordering;
|
||||
|
||||
use mime::Mime;
|
||||
|
||||
use crate::header::{qitem, QualityItem};
|
||||
|
@ -97,14 +99,14 @@ header! {
|
|||
test_header!(
|
||||
test1,
|
||||
vec![b"audio/*; q=0.2, audio/basic"],
|
||||
Some(HeaderField(vec![
|
||||
Some(Accept(vec![
|
||||
QualityItem::new("audio/*".parse().unwrap(), q(200)),
|
||||
qitem("audio/basic".parse().unwrap()),
|
||||
])));
|
||||
test_header!(
|
||||
test2,
|
||||
vec![b"text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"],
|
||||
Some(HeaderField(vec![
|
||||
Some(Accept(vec![
|
||||
QualityItem::new(mime::TEXT_PLAIN, q(500)),
|
||||
qitem(mime::TEXT_HTML),
|
||||
QualityItem::new(
|
||||
|
@ -138,23 +140,148 @@ header! {
|
|||
}
|
||||
|
||||
impl Accept {
|
||||
/// A constructor to easily create `Accept: */*`.
|
||||
/// Construct `Accept: */*`.
|
||||
pub fn star() -> Accept {
|
||||
Accept(vec![qitem(mime::STAR_STAR)])
|
||||
}
|
||||
|
||||
/// A constructor to easily create `Accept: application/json`.
|
||||
/// Construct `Accept: application/json`.
|
||||
pub fn json() -> Accept {
|
||||
Accept(vec![qitem(mime::APPLICATION_JSON)])
|
||||
}
|
||||
|
||||
/// A constructor to easily create `Accept: text/*`.
|
||||
/// Construct `Accept: text/*`.
|
||||
pub fn text() -> Accept {
|
||||
Accept(vec![qitem(mime::TEXT_STAR)])
|
||||
}
|
||||
|
||||
/// A constructor to easily create `Accept: image/*`.
|
||||
/// Construct `Accept: image/*`.
|
||||
pub fn image() -> Accept {
|
||||
Accept(vec![qitem(mime::IMAGE_STAR)])
|
||||
}
|
||||
|
||||
/// Construct `Accept: text/html`.
|
||||
pub fn html() -> Accept {
|
||||
Accept(vec![qitem(mime::TEXT_HTML)])
|
||||
}
|
||||
|
||||
/// Returns a sorted list of mime types from highest to lowest preference, accounting for
|
||||
/// [q-factor weighting] and specificity.
|
||||
///
|
||||
/// [q-factor weighting]: https://tools.ietf.org/html/rfc7231#section-5.3.2
|
||||
pub fn mime_precedence(&self) -> Vec<Mime> {
|
||||
let mut types = self.0.clone();
|
||||
|
||||
// use stable sort so items with equal q-factor and specificity retain listed order
|
||||
types.sort_by(|a, b| {
|
||||
// sort by q-factor descending
|
||||
b.quality.cmp(&a.quality).then_with(|| {
|
||||
// use specificity rules on mime types with
|
||||
// same q-factor (eg. text/html > text/* > */*)
|
||||
|
||||
// subtypes are not comparable if main type is star, so return
|
||||
match (a.item.type_(), b.item.type_()) {
|
||||
(mime::STAR, mime::STAR) => return Ordering::Equal,
|
||||
|
||||
// a is sorted after b
|
||||
(mime::STAR, _) => return Ordering::Greater,
|
||||
|
||||
// a is sorted before b
|
||||
(_, mime::STAR) => return Ordering::Less,
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// in both these match expressions, the returned ordering appears
|
||||
// inverted because sort is high-to-low ("descending") precedence
|
||||
match (a.item.subtype(), b.item.subtype()) {
|
||||
(mime::STAR, mime::STAR) => Ordering::Equal,
|
||||
|
||||
// a is sorted after b
|
||||
(mime::STAR, _) => Ordering::Greater,
|
||||
|
||||
// a is sorted before b
|
||||
(_, mime::STAR) => Ordering::Less,
|
||||
|
||||
_ => Ordering::Equal,
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
types.into_iter().map(|qitem| qitem.item).collect()
|
||||
}
|
||||
|
||||
/// Extracts the most preferable mime type, accounting for [q-factor weighting].
|
||||
///
|
||||
/// If no q-factors are provided, the first mime type is chosen. Note that items without
|
||||
/// q-factors are given the maximum preference value.
|
||||
///
|
||||
/// Returns `None` if contained list is empty.
|
||||
///
|
||||
/// [q-factor weighting]: https://tools.ietf.org/html/rfc7231#section-5.3.2
|
||||
pub fn mime_preference(&self) -> Option<Mime> {
|
||||
let types = self.mime_precedence();
|
||||
types.first().cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::header::q;
|
||||
|
||||
#[test]
|
||||
fn test_mime_precedence() {
|
||||
let test = Accept(vec![]);
|
||||
assert!(test.mime_precedence().is_empty());
|
||||
|
||||
let test = Accept(vec![qitem(mime::APPLICATION_JSON)]);
|
||||
assert_eq!(test.mime_precedence(), vec!(mime::APPLICATION_JSON));
|
||||
|
||||
let test = Accept(vec![
|
||||
qitem(mime::TEXT_HTML),
|
||||
"application/xhtml+xml".parse().unwrap(),
|
||||
QualityItem::new("application/xml".parse().unwrap(), q(0.9)),
|
||||
QualityItem::new(mime::STAR_STAR, q(0.8)),
|
||||
]);
|
||||
assert_eq!(
|
||||
test.mime_precedence(),
|
||||
vec![
|
||||
mime::TEXT_HTML,
|
||||
"application/xhtml+xml".parse().unwrap(),
|
||||
"application/xml".parse().unwrap(),
|
||||
mime::STAR_STAR,
|
||||
]
|
||||
);
|
||||
|
||||
let test = Accept(vec![
|
||||
qitem(mime::STAR_STAR),
|
||||
qitem(mime::IMAGE_STAR),
|
||||
qitem(mime::IMAGE_PNG),
|
||||
]);
|
||||
assert_eq!(
|
||||
test.mime_precedence(),
|
||||
vec![mime::IMAGE_PNG, mime::IMAGE_STAR, mime::STAR_STAR]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mime_preference() {
|
||||
let test = Accept(vec![
|
||||
qitem(mime::TEXT_HTML),
|
||||
"application/xhtml+xml".parse().unwrap(),
|
||||
QualityItem::new("application/xml".parse().unwrap(), q(0.9)),
|
||||
QualityItem::new(mime::STAR_STAR, q(0.8)),
|
||||
]);
|
||||
assert_eq!(test.mime_preference(), Some(mime::TEXT_HTML));
|
||||
|
||||
let test = Accept(vec![
|
||||
QualityItem::new("video/*".parse().unwrap(), q(0.8)),
|
||||
qitem(mime::IMAGE_PNG),
|
||||
QualityItem::new(mime::STAR_STAR, q(0.5)),
|
||||
qitem(mime::IMAGE_SVG),
|
||||
QualityItem::new(mime::IMAGE_STAR, q(0.8)),
|
||||
]);
|
||||
assert_eq!(test.mime_preference(), Some(mime::IMAGE_PNG));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -550,8 +550,7 @@ impl fmt::Display for ContentDisposition {
|
|||
write!(f, "{}", self.disposition)?;
|
||||
self.parameters
|
||||
.iter()
|
||||
.map(|param| write!(f, "; {}", param))
|
||||
.collect()
|
||||
.try_for_each(|param| write!(f, "; {}", param))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -370,9 +370,7 @@ impl fmt::Display for ExtendedValue {
|
|||
}
|
||||
|
||||
/// Percent encode a sequence of bytes with a character set defined in
|
||||
/// [https://tools.ietf.org/html/rfc5987#section-3.2][url]
|
||||
///
|
||||
/// [url]: https://tools.ietf.org/html/rfc5987#section-3.2
|
||||
/// <https://tools.ietf.org/html/rfc5987#section-3.2>
|
||||
pub fn http_percent_encode(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result {
|
||||
let encoded = percent_encoding::percent_encode(bytes, HTTP_VALUE);
|
||||
fmt::Display::fmt(&encoded, f)
|
||||
|
|
|
@ -7,9 +7,7 @@ use self::Charset::*;
|
|||
///
|
||||
/// The string representation is normalized to upper case.
|
||||
///
|
||||
/// See [http://www.iana.org/assignments/character-sets/character-sets.xhtml][url].
|
||||
///
|
||||
/// [url]: http://www.iana.org/assignments/character-sets/character-sets.xhtml
|
||||
/// See <http://www.iana.org/assignments/character-sets/character-sets.xhtml>.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum Charset {
|
||||
|
|
|
@ -7,10 +7,12 @@ use crate::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValue, Writer};
|
|||
/// 1. `%x21`, or
|
||||
/// 2. in the range `%x23` to `%x7E`, or
|
||||
/// 3. above `%x80`
|
||||
fn entity_validate_char(c: u8) -> bool {
|
||||
c == 0x21 || (0x23..=0x7e).contains(&c) || (c >= 0x80)
|
||||
}
|
||||
|
||||
fn check_slice_validity(slice: &str) -> bool {
|
||||
slice
|
||||
.bytes()
|
||||
.all(|c| c == b'\x21' || (c >= b'\x23' && c <= b'\x7e') | (c >= b'\x80'))
|
||||
slice.bytes().all(entity_validate_char)
|
||||
}
|
||||
|
||||
/// An entity tag, defined in [RFC7232](https://tools.ietf.org/html/rfc7232#section-2.3)
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
use std::{cmp, fmt, str};
|
||||
use std::{
|
||||
cmp,
|
||||
convert::{TryFrom, TryInto},
|
||||
fmt, str,
|
||||
};
|
||||
|
||||
use self::internal::IntoQuality;
|
||||
use derive_more::{Display, Error};
|
||||
|
||||
const MAX_QUALITY: u16 = 1000;
|
||||
const MAX_FLOAT_QUALITY: f32 = 1.0;
|
||||
|
||||
/// Represents a quality used in quality values.
|
||||
///
|
||||
/// Can be created with the `q` function.
|
||||
/// Can be created with the [`q`] function.
|
||||
///
|
||||
/// # Implementation notes
|
||||
///
|
||||
|
@ -18,12 +25,54 @@ use self::internal::IntoQuality;
|
|||
///
|
||||
/// [RFC7231 Section 5.3.1](https://tools.ietf.org/html/rfc7231#section-5.3.1)
|
||||
/// gives more information on quality values in HTTP header fields.
|
||||
#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Quality(u16);
|
||||
|
||||
impl Quality {
|
||||
/// # Panics
|
||||
/// Panics in debug mode when value is not in the range 0.0 <= n <= 1.0.
|
||||
fn from_f32(value: f32) -> Self {
|
||||
// Check that `value` is within range should be done before calling this method.
|
||||
// Just in case, this debug_assert should catch if we were forgetful.
|
||||
debug_assert!(
|
||||
(0.0f32..=1.0f32).contains(&value),
|
||||
"q value must be between 0.0 and 1.0"
|
||||
);
|
||||
|
||||
Quality((value * MAX_QUALITY as f32) as u16)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Quality {
|
||||
fn default() -> Quality {
|
||||
Quality(1000)
|
||||
Quality(MAX_QUALITY)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Display, Error)]
|
||||
pub struct QualityOutOfBounds;
|
||||
|
||||
impl TryFrom<u16> for Quality {
|
||||
type Error = QualityOutOfBounds;
|
||||
|
||||
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||
if (0..=MAX_QUALITY).contains(&value) {
|
||||
Ok(Quality(value))
|
||||
} else {
|
||||
Err(QualityOutOfBounds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<f32> for Quality {
|
||||
type Error = QualityOutOfBounds;
|
||||
|
||||
fn try_from(value: f32) -> Result<Self, Self::Error> {
|
||||
if (0.0..=MAX_FLOAT_QUALITY).contains(&value) {
|
||||
Ok(Quality::from_f32(value))
|
||||
} else {
|
||||
Err(QualityOutOfBounds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,8 +104,9 @@ impl<T: PartialEq> cmp::PartialOrd for QualityItem<T> {
|
|||
impl<T: fmt::Display> fmt::Display for QualityItem<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Display::fmt(&self.item, f)?;
|
||||
|
||||
match self.quality.0 {
|
||||
1000 => Ok(()),
|
||||
MAX_QUALITY => Ok(()),
|
||||
0 => f.write_str("; q=0"),
|
||||
x => write!(f, "; q=0.{}", format!("{:03}", x).trim_end_matches('0')),
|
||||
}
|
||||
|
@ -66,105 +116,79 @@ impl<T: fmt::Display> fmt::Display for QualityItem<T> {
|
|||
impl<T: str::FromStr> str::FromStr for QualityItem<T> {
|
||||
type Err = crate::error::ParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<QualityItem<T>, crate::error::ParseError> {
|
||||
if !s.is_ascii() {
|
||||
fn from_str(qitem_str: &str) -> Result<QualityItem<T>, crate::error::ParseError> {
|
||||
if !qitem_str.is_ascii() {
|
||||
return Err(crate::error::ParseError::Header);
|
||||
}
|
||||
|
||||
// Set defaults used if parsing fails.
|
||||
let mut raw_item = s;
|
||||
let mut raw_item = qitem_str;
|
||||
let mut quality = 1f32;
|
||||
|
||||
let parts: Vec<&str> = s.rsplitn(2, ';').map(|x| x.trim()).collect();
|
||||
let parts: Vec<_> = qitem_str.rsplitn(2, ';').map(str::trim).collect();
|
||||
|
||||
if parts.len() == 2 {
|
||||
// example for item with q-factor:
|
||||
//
|
||||
// gzip; q=0.65
|
||||
// ^^^^^^ parts[0]
|
||||
// ^^ start
|
||||
// ^^^^ q_val
|
||||
// ^^^^ parts[1]
|
||||
|
||||
if parts[0].len() < 2 {
|
||||
// Can't possibly be an attribute since an attribute needs at least a name followed
|
||||
// by an equals sign. And bare identifiers are forbidden.
|
||||
return Err(crate::error::ParseError::Header);
|
||||
}
|
||||
|
||||
let start = &parts[0][0..2];
|
||||
|
||||
if start == "q=" || start == "Q=" {
|
||||
let q_part = &parts[0][2..parts[0].len()];
|
||||
if q_part.len() > 5 {
|
||||
let q_val = &parts[0][2..];
|
||||
if q_val.len() > 5 {
|
||||
// longer than 5 indicates an over-precise q-factor
|
||||
return Err(crate::error::ParseError::Header);
|
||||
}
|
||||
match q_part.parse::<f32>() {
|
||||
Ok(q_value) => {
|
||||
if 0f32 <= q_value && q_value <= 1f32 {
|
||||
|
||||
let q_value = q_val
|
||||
.parse::<f32>()
|
||||
.map_err(|_| crate::error::ParseError::Header)?;
|
||||
|
||||
if (0f32..=1f32).contains(&q_value) {
|
||||
quality = q_value;
|
||||
raw_item = parts[1];
|
||||
} else {
|
||||
return Err(crate::error::ParseError::Header);
|
||||
}
|
||||
}
|
||||
Err(_) => return Err(crate::error::ParseError::Header),
|
||||
}
|
||||
}
|
||||
}
|
||||
match raw_item.parse::<T>() {
|
||||
// we already checked above that the quality is within range
|
||||
Ok(item) => Ok(QualityItem::new(item, from_f32(quality))),
|
||||
Err(_) => Err(crate::error::ParseError::Header),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn from_f32(f: f32) -> Quality {
|
||||
// this function is only used internally. A check that `f` is within range
|
||||
// should be done before calling this method. Just in case, this
|
||||
// debug_assert should catch if we were forgetful
|
||||
debug_assert!(
|
||||
f >= 0f32 && f <= 1f32,
|
||||
"q value must be between 0.0 and 1.0"
|
||||
);
|
||||
Quality((f * 1000f32) as u16)
|
||||
let item = raw_item
|
||||
.parse::<T>()
|
||||
.map_err(|_| crate::error::ParseError::Header)?;
|
||||
|
||||
// we already checked above that the quality is within range
|
||||
Ok(QualityItem::new(item, Quality::from_f32(quality)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience function to wrap a value in a `QualityItem`
|
||||
/// Sets `q` to the default 1.0
|
||||
pub fn qitem<T>(item: T) -> QualityItem<T> {
|
||||
QualityItem::new(item, Default::default())
|
||||
QualityItem::new(item, Quality::default())
|
||||
}
|
||||
|
||||
/// Convenience function to create a `Quality` from a float or integer.
|
||||
///
|
||||
/// Implemented for `u16` and `f32`. Panics if value is out of range.
|
||||
pub fn q<T: IntoQuality>(val: T) -> Quality {
|
||||
val.into_quality()
|
||||
}
|
||||
|
||||
mod internal {
|
||||
use super::Quality;
|
||||
|
||||
// TryFrom is probably better, but it's not stable. For now, we want to
|
||||
// keep the functionality of the `q` function, while allowing it to be
|
||||
// generic over `f32` and `u16`.
|
||||
//
|
||||
// `q` would panic before, so keep that behavior. `TryFrom` can be
|
||||
// introduced later for a non-panicking conversion.
|
||||
|
||||
pub trait IntoQuality: Sealed + Sized {
|
||||
fn into_quality(self) -> Quality;
|
||||
}
|
||||
|
||||
impl IntoQuality for f32 {
|
||||
fn into_quality(self) -> Quality {
|
||||
assert!(
|
||||
self >= 0f32 && self <= 1f32,
|
||||
"float must be between 0.0 and 1.0"
|
||||
);
|
||||
super::from_f32(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoQuality for u16 {
|
||||
fn into_quality(self) -> Quality {
|
||||
assert!(self <= 1000, "u16 must be between 0 and 1000");
|
||||
Quality(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Sealed {}
|
||||
impl Sealed for u16 {}
|
||||
impl Sealed for f32 {}
|
||||
pub fn q<T>(val: T) -> Quality
|
||||
where
|
||||
T: TryInto<Quality>,
|
||||
T::Error: fmt::Debug,
|
||||
{
|
||||
// TODO: on next breaking change, handle unwrap differently
|
||||
val.try_into().unwrap()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -270,15 +294,13 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic] // FIXME - 32-bit msvc unwinding broken
|
||||
#[cfg_attr(all(target_arch = "x86", target_env = "msvc"), ignore)]
|
||||
#[should_panic]
|
||||
fn test_quality_invalid() {
|
||||
q(-1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic] // FIXME - 32-bit msvc unwinding broken
|
||||
#[cfg_attr(all(target_arch = "x86", target_env = "msvc"), ignore)]
|
||||
#[should_panic]
|
||||
fn test_quality_invalid2() {
|
||||
q(2.0);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,19 @@
|
|||
# Changes
|
||||
|
||||
## Unreleased - 2020-xx-xx
|
||||
|
||||
|
||||
## 2.0.3 - 2020-11-29
|
||||
### Fixed
|
||||
* Ensure `actix-http` dependency uses same `serde_urlencoded`.
|
||||
|
||||
|
||||
## 2.0.2 - 2020-11-25
|
||||
### Changed
|
||||
* Upgrade `serde_urlencoded` to `0.7`.
|
||||
* Upgrade `serde_urlencoded` to `0.7`. [#1773]
|
||||
|
||||
[#1773]: https://github.com/actix/actix-web/pull/1773
|
||||
|
||||
|
||||
## 2.0.1 - 2020-10-30
|
||||
### Changed
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "awc"
|
||||
version = "2.0.1"
|
||||
version = "2.0.3"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Async HTTP and WebSocket client library built on the Actix ecosystem"
|
||||
readme = "README.md"
|
||||
|
@ -39,7 +39,7 @@ compress = ["actix-http/compress"]
|
|||
[dependencies]
|
||||
actix-codec = "0.3.0"
|
||||
actix-service = "1.0.6"
|
||||
actix-http = "2.0.0"
|
||||
actix-http = "2.2.0"
|
||||
actix-rt = "1.0.0"
|
||||
|
||||
base64 = "0.13"
|
||||
|
|
|
@ -3,14 +3,14 @@
|
|||
> Async HTTP and WebSocket client library.
|
||||
|
||||
[](https://crates.io/crates/awc)
|
||||
[](https://docs.rs/awc/2.0.1)
|
||||
[](https://docs.rs/awc/2.0.3)
|
||||

|
||||
[](https://deps.rs/crate/awc/2.0.1)
|
||||
[](https://deps.rs/crate/awc/2.0.3)
|
||||
[](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
## Documentation & Resources
|
||||
|
||||
- [API Documentation](https://docs.rs/awc/2.0.1)
|
||||
- [API Documentation](https://docs.rs/awc)
|
||||
- [Example Project](https://github.com/actix/examples/tree/HEAD/awc_https)
|
||||
- [Chat on Gitter](https://gitter.im/actix/actix-web)
|
||||
- Minimum Supported Rust Version (MSRV): 1.42.0
|
||||
|
|
|
@ -70,9 +70,14 @@ impl WebsocketsRequest {
|
|||
<Uri as TryFrom<U>>::Error: Into<HttpError>,
|
||||
{
|
||||
let mut err = None;
|
||||
|
||||
#[allow(clippy::field_reassign_with_default)]
|
||||
let mut head = {
|
||||
let mut head = RequestHead::default();
|
||||
head.method = Method::GET;
|
||||
head.version = Version::HTTP_11;
|
||||
head
|
||||
};
|
||||
|
||||
match Uri::try_from(uri) {
|
||||
Ok(uri) => head.uri = uri,
|
||||
|
|
|
@ -1,3 +1,12 @@
|
|||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
threshold: 10% # make CI green
|
||||
patch:
|
||||
default:
|
||||
threshold: 10% # make CI green
|
||||
|
||||
ignore: # ignore code coverage on following paths
|
||||
- "**/tests"
|
||||
- "test-server"
|
||||
|
|
|
@ -2,29 +2,31 @@ digraph {
|
|||
subgraph cluster_web {
|
||||
label="actix/actix-web"
|
||||
"awc"
|
||||
"actix-web"
|
||||
"actix-files"
|
||||
"actix-http"
|
||||
"actix-multipart"
|
||||
"actix-web-actors"
|
||||
"actix-web-codegen"
|
||||
"web"
|
||||
"files"
|
||||
"http"
|
||||
"multipart"
|
||||
"web-actors"
|
||||
"codegen"
|
||||
"http-test"
|
||||
}
|
||||
|
||||
"actix-web" -> { "actix-codec" "actix-service" "actix-utils" "actix-router" "actix-rt" "actix-server" "actix-testing" "actix-macros" "actix-threadpool" "actix-tls" "actix-web-codegen" "actix-http" "awc" }
|
||||
"awc" -> { "actix-codec" "actix-service" "actix-http" "actix-rt" }
|
||||
"actix-web-actors" -> { "actix" "actix-web" "actix-http" "actix-codec" }
|
||||
"actix-multipart" -> { "actix-web" "actix-service" "actix-utils" }
|
||||
"actix-http" -> { "actix-service" "actix-codec" "actix-connect" "actix-utils" "actix-rt" "actix-threadpool" }
|
||||
"actix-http" -> { "actix" "actix-tls" }[color=blue] // optional
|
||||
"actix-files" -> { "actix-web" "actix-http" }
|
||||
"web" -> { "codec" "service" "utils" "router" "rt" "server" "testing" "macros" "threadpool" "tls" "codegen" "http" "awc" }
|
||||
"awc" -> { "codec" "service" "http" "rt" }
|
||||
"web-actors" -> { "actix" "web" "http" "codec" }
|
||||
"multipart" -> { "web" "service" "utils" }
|
||||
"http" -> { "service" "codec" "connect" "utils" "rt" "threadpool" }
|
||||
"http" -> { "actix" "tls" }[color=blue] // optional
|
||||
"files" -> { "web" }
|
||||
"http-test" -> { "service" "codec" "connect" "utils" "rt" "server" "testing" "awc" }
|
||||
|
||||
// net
|
||||
|
||||
"actix-utils" -> { "actix-service" "actix-rt" "actix-codec" }
|
||||
"actix-tracing" -> { "actix-service" }
|
||||
"actix-tls" -> { "actix-service" "actix-codec" "actix-utils" }
|
||||
"actix-testing" -> { "actix-rt" "actix-macros" "actix-server" "actix-service" }
|
||||
"actix-server" -> { "actix-service" "actix-rt" "actix-codec" "actix-utils" }
|
||||
"actix-rt" -> { "actix-macros" "actix-threadpool" }
|
||||
"actix-connect" -> { "actix-service" "actix-codec" "actix-utils" "actix-rt" }
|
||||
"utils" -> { "service" "rt" "codec" }
|
||||
"tracing" -> { "service" }
|
||||
"tls" -> { "service" "codec" "utils" }
|
||||
"testing" -> { "rt" "macros" "server" "service" }
|
||||
"server" -> { "service" "rt" "codec" "utils" }
|
||||
"rt" -> { "macros" "threadpool" }
|
||||
"connect" -> { "service" "codec" "utils" "rt" }
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ digraph {
|
|||
"actix-multipart"
|
||||
"actix-web-actors"
|
||||
"actix-web-codegen"
|
||||
"actix-http-test"
|
||||
}
|
||||
|
||||
"actix-web" -> { "actix-web-codegen" "actix-http" "awc" }
|
||||
|
@ -15,5 +16,6 @@ digraph {
|
|||
"actix-web-actors" -> { "actix" "actix-web" "actix-http" }
|
||||
"actix-multipart" -> { "actix-web" }
|
||||
"actix-http" -> { "actix" }[color=blue] // optional
|
||||
"actix-files" -> { "actix-web" "actix-http" }
|
||||
"actix-files" -> { "actix-web" }
|
||||
"actix-http-test" -> { "awc" }
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
//! Error and Result module
|
||||
|
||||
pub use actix_http::error::*;
|
||||
use derive_more::{Display, From};
|
||||
use serde_json::error::Error as JsonError;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
//! Actix web is a powerful, pragmatic, and extremely fast web framework for Rust.
|
||||
//! Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust.
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
|
@ -102,10 +102,11 @@ pub use crate::app::App;
|
|||
pub use crate::extract::FromRequest;
|
||||
pub use crate::request::HttpRequest;
|
||||
pub use crate::resource::Resource;
|
||||
pub use crate::responder::{Either, Responder};
|
||||
pub use crate::responder::Responder;
|
||||
pub use crate::route::Route;
|
||||
pub use crate::scope::Scope;
|
||||
pub use crate::server::HttpServer;
|
||||
pub use crate::types::{Either, EitherExtractError};
|
||||
|
||||
pub mod dev {
|
||||
//! The `actix-web` prelude for library developers
|
||||
|
|
|
@ -192,10 +192,7 @@ impl AcceptEncoding {
|
|||
};
|
||||
let quality = match parts.len() {
|
||||
1 => encoding.quality(),
|
||||
_ => match f64::from_str(parts[1]) {
|
||||
Ok(q) => q,
|
||||
Err(_) => 0.0,
|
||||
},
|
||||
_ => f64::from_str(parts[1]).unwrap_or(0.0),
|
||||
};
|
||||
Some(AcceptEncoding { encoding, quality })
|
||||
}
|
||||
|
|
|
@ -105,6 +105,7 @@ mod tests {
|
|||
use crate::test::{self, TestRequest};
|
||||
use crate::HttpResponse;
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn render_500<B>(mut res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
|
||||
res.response_mut()
|
||||
.headers_mut()
|
||||
|
|
|
@ -154,6 +154,7 @@ mod tests {
|
|||
use crate::test::{self, TestRequest};
|
||||
use crate::HttpResponse;
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn render_500<B>(mut res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
|
||||
res.response_mut()
|
||||
.headers_mut()
|
||||
|
|
|
@ -137,9 +137,9 @@ where
|
|||
// so the change can not be deduced from the length comparison
|
||||
if path != original_path {
|
||||
let mut parts = head.uri.clone().into_parts();
|
||||
let pq = parts.path_and_query.as_ref().unwrap();
|
||||
let query = parts.path_and_query.as_ref().and_then(|pq| pq.query());
|
||||
|
||||
let path = if let Some(q) = pq.query() {
|
||||
let path = if let Some(q) = query {
|
||||
Bytes::from(format!("{}?{}", path, q))
|
||||
} else {
|
||||
Bytes::copy_from_slice(path.as_bytes())
|
||||
|
|
|
@ -675,4 +675,40 @@ mod tests {
|
|||
let res = call_service(&mut srv, req).await;
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn extract_path_pattern_complex() {
|
||||
let mut srv = init_service(
|
||||
App::new()
|
||||
.service(web::scope("/user").service(web::scope("/{id}").service(
|
||||
web::resource("").to(move |req: HttpRequest| {
|
||||
assert_eq!(req.match_pattern(), Some("/user/{id}".to_owned()));
|
||||
|
||||
HttpResponse::Ok().finish()
|
||||
}),
|
||||
)))
|
||||
.service(web::resource("/").to(move |req: HttpRequest| {
|
||||
assert_eq!(req.match_pattern(), Some("/".to_owned()));
|
||||
|
||||
HttpResponse::Ok().finish()
|
||||
}))
|
||||
.default_service(web::to(move |req: HttpRequest| {
|
||||
assert!(req.match_pattern().is_none());
|
||||
HttpResponse::Ok().finish()
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
|
||||
let req = TestRequest::get().uri("/user/test").to_request();
|
||||
let res = call_service(&mut srv, req).await;
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let req = TestRequest::get().uri("/").to_request();
|
||||
let res = call_service(&mut srv, req).await;
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let req = TestRequest::get().uri("/not-exist").to_request();
|
||||
let res = call_service(&mut srv, req).await;
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -332,82 +332,6 @@ impl<T: Responder> Future for CustomResponderFut<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Combines two different responder types into a single type
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_web::{Either, Error, HttpResponse};
|
||||
///
|
||||
/// type RegisterResult = Either<HttpResponse, Result<HttpResponse, Error>>;
|
||||
///
|
||||
/// fn index() -> RegisterResult {
|
||||
/// if is_a_variant() {
|
||||
/// // <- choose left variant
|
||||
/// Either::A(HttpResponse::BadRequest().body("Bad data"))
|
||||
/// } else {
|
||||
/// Either::B(
|
||||
/// // <- Right variant
|
||||
/// Ok(HttpResponse::Ok()
|
||||
/// .content_type("text/html")
|
||||
/// .body("Hello!"))
|
||||
/// )
|
||||
/// }
|
||||
/// }
|
||||
/// # fn is_a_variant() -> bool { true }
|
||||
/// # fn main() {}
|
||||
/// ```
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Either<A, B> {
|
||||
/// First branch of the type
|
||||
A(A),
|
||||
/// Second branch of the type
|
||||
B(B),
|
||||
}
|
||||
|
||||
impl<A, B> Responder for Either<A, B>
|
||||
where
|
||||
A: Responder,
|
||||
B: Responder,
|
||||
{
|
||||
type Error = Error;
|
||||
type Future = EitherResponder<A, B>;
|
||||
|
||||
fn respond_to(self, req: &HttpRequest) -> Self::Future {
|
||||
match self {
|
||||
Either::A(a) => EitherResponder::A(a.respond_to(req)),
|
||||
Either::B(b) => EitherResponder::B(b.respond_to(req)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project(project = EitherResponderProj)]
|
||||
pub enum EitherResponder<A, B>
|
||||
where
|
||||
A: Responder,
|
||||
B: Responder,
|
||||
{
|
||||
A(#[pin] A::Future),
|
||||
B(#[pin] B::Future),
|
||||
}
|
||||
|
||||
impl<A, B> Future for EitherResponder<A, B>
|
||||
where
|
||||
A: Responder,
|
||||
B: Responder,
|
||||
{
|
||||
type Output = Result<Response, Error>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
match self.project() {
|
||||
EitherResponderProj::A(fut) => {
|
||||
Poll::Ready(ready!(fut.poll(cx)).map_err(|e| e.into()))
|
||||
}
|
||||
EitherResponderProj::B(fut) => {
|
||||
Poll::Ready(ready!(fut.poll(cx).map_err(|e| e.into())))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Responder for InternalError<T>
|
||||
where
|
||||
T: std::fmt::Debug + std::fmt::Display + 'static,
|
||||
|
|
|
@ -86,7 +86,7 @@ impl ResourceMap {
|
|||
if let Some(plen) = pattern.is_prefix_match(path) {
|
||||
return rmap.has_resource(&path[plen..]);
|
||||
}
|
||||
} else if pattern.is_match(path) {
|
||||
} else if pattern.is_match(path) || pattern.pattern() == "" && path == "/" {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
44
src/route.rs
44
src/route.rs
|
@ -16,24 +16,24 @@ use crate::responder::Responder;
|
|||
use crate::service::{ServiceRequest, ServiceResponse};
|
||||
use crate::HttpResponse;
|
||||
|
||||
type BoxedRouteService<Req, Res> = Box<
|
||||
type BoxedRouteService = Box<
|
||||
dyn Service<
|
||||
Request = Req,
|
||||
Response = Res,
|
||||
Request = ServiceRequest,
|
||||
Response = ServiceResponse,
|
||||
Error = Error,
|
||||
Future = LocalBoxFuture<'static, Result<Res, Error>>,
|
||||
Future = LocalBoxFuture<'static, Result<ServiceResponse, Error>>,
|
||||
>,
|
||||
>;
|
||||
|
||||
type BoxedRouteNewService<Req, Res> = Box<
|
||||
type BoxedRouteNewService = Box<
|
||||
dyn ServiceFactory<
|
||||
Config = (),
|
||||
Request = Req,
|
||||
Response = Res,
|
||||
Request = ServiceRequest,
|
||||
Response = ServiceResponse,
|
||||
Error = Error,
|
||||
InitError = (),
|
||||
Service = BoxedRouteService<Req, Res>,
|
||||
Future = LocalBoxFuture<'static, Result<BoxedRouteService<Req, Res>, ()>>,
|
||||
Service = BoxedRouteService,
|
||||
Future = LocalBoxFuture<'static, Result<BoxedRouteService, ()>>,
|
||||
>,
|
||||
>;
|
||||
|
||||
|
@ -42,7 +42,7 @@ type BoxedRouteNewService<Req, Res> = Box<
|
|||
/// Route uses builder-like pattern for configuration.
|
||||
/// If handler is not explicitly set, default *404 Not Found* handler is used.
|
||||
pub struct Route {
|
||||
service: BoxedRouteNewService<ServiceRequest, ServiceResponse>,
|
||||
service: BoxedRouteNewService,
|
||||
guards: Rc<Vec<Box<dyn Guard>>>,
|
||||
}
|
||||
|
||||
|
@ -80,15 +80,8 @@ impl ServiceFactory for Route {
|
|||
}
|
||||
}
|
||||
|
||||
type RouteFuture = LocalBoxFuture<
|
||||
'static,
|
||||
Result<BoxedRouteService<ServiceRequest, ServiceResponse>, ()>,
|
||||
>;
|
||||
|
||||
#[pin_project::pin_project]
|
||||
pub struct CreateRouteService {
|
||||
#[pin]
|
||||
fut: RouteFuture,
|
||||
fut: LocalBoxFuture<'static, Result<BoxedRouteService, ()>>,
|
||||
guards: Rc<Vec<Box<dyn Guard>>>,
|
||||
}
|
||||
|
||||
|
@ -96,9 +89,9 @@ impl Future for CreateRouteService {
|
|||
type Output = Result<RouteService, ()>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
let this = self.get_mut();
|
||||
|
||||
match this.fut.poll(cx)? {
|
||||
match this.fut.as_mut().poll(cx)? {
|
||||
Poll::Ready(service) => Poll::Ready(Ok(RouteService {
|
||||
service,
|
||||
guards: this.guards.clone(),
|
||||
|
@ -109,7 +102,7 @@ impl Future for CreateRouteService {
|
|||
}
|
||||
|
||||
pub struct RouteService {
|
||||
service: BoxedRouteService<ServiceRequest, ServiceResponse>,
|
||||
service: BoxedRouteService,
|
||||
guards: Rc<Vec<Box<dyn Guard>>>,
|
||||
}
|
||||
|
||||
|
@ -135,7 +128,7 @@ impl Service for RouteService {
|
|||
}
|
||||
|
||||
fn call(&mut self, req: ServiceRequest) -> Self::Future {
|
||||
self.service.call(req).boxed_local()
|
||||
self.service.call(req)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -275,12 +268,12 @@ where
|
|||
T::Service: 'static,
|
||||
<T::Service as Service>::Future: 'static,
|
||||
{
|
||||
type Config = ();
|
||||
type Request = ServiceRequest;
|
||||
type Response = ServiceResponse;
|
||||
type Error = Error;
|
||||
type Config = ();
|
||||
type Service = BoxedRouteService;
|
||||
type InitError = ();
|
||||
type Service = BoxedRouteService<ServiceRequest, Self::Response>;
|
||||
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
|
||||
|
||||
fn new_service(&self, _: ()) -> Self::Future {
|
||||
|
@ -288,8 +281,7 @@ where
|
|||
.new_service(())
|
||||
.map(|result| match result {
|
||||
Ok(service) => {
|
||||
let service: BoxedRouteService<_, _> =
|
||||
Box::new(RouteServiceWrapper { service });
|
||||
let service = Box::new(RouteServiceWrapper { service }) as _;
|
||||
Ok(service)
|
||||
}
|
||||
Err(_) => Err(()),
|
||||
|
|
|
@ -269,8 +269,9 @@ where
|
|||
{
|
||||
let body = read_body(res).await;
|
||||
|
||||
serde_json::from_slice(&body)
|
||||
.unwrap_or_else(|_| panic!("read_response_json failed during deserialization"))
|
||||
serde_json::from_slice(&body).unwrap_or_else(|e| {
|
||||
panic!("read_response_json failed during deserialization: {}", e)
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn load_stream<S>(mut stream: S) -> Result<Bytes, Error>
|
||||
|
|
|
@ -0,0 +1,274 @@
|
|||
use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use actix_http::{Error, Response};
|
||||
use bytes::Bytes;
|
||||
use futures_util::{future::LocalBoxFuture, ready, FutureExt, TryFutureExt};
|
||||
use pin_project::pin_project;
|
||||
|
||||
use crate::{dev, request::HttpRequest, FromRequest, Responder};
|
||||
|
||||
/// Combines two different responder types into a single type
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_web::{Either, Error, HttpResponse};
|
||||
///
|
||||
/// type RegisterResult = Either<HttpResponse, Result<HttpResponse, Error>>;
|
||||
///
|
||||
/// fn index() -> RegisterResult {
|
||||
/// if is_a_variant() {
|
||||
/// // <- choose left variant
|
||||
/// Either::A(HttpResponse::BadRequest().body("Bad data"))
|
||||
/// } else {
|
||||
/// Either::B(
|
||||
/// // <- Right variant
|
||||
/// Ok(HttpResponse::Ok()
|
||||
/// .content_type("text/html")
|
||||
/// .body("Hello!"))
|
||||
/// )
|
||||
/// }
|
||||
/// }
|
||||
/// # fn is_a_variant() -> bool { true }
|
||||
/// # fn main() {}
|
||||
/// ```
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Either<A, B> {
|
||||
/// First branch of the type
|
||||
A(A),
|
||||
/// Second branch of the type
|
||||
B(B),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl<A, B> Either<A, B> {
|
||||
pub(self) fn unwrap_left(self) -> A {
|
||||
match self {
|
||||
Either::A(data) => data,
|
||||
Either::B(_) => {
|
||||
panic!("Cannot unwrap left branch. Either contains a right branch.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(self) fn unwrap_right(self) -> B {
|
||||
match self {
|
||||
Either::A(_) => {
|
||||
panic!("Cannot unwrap right branch. Either contains a left branch.")
|
||||
}
|
||||
Either::B(data) => data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, B> Responder for Either<A, B>
|
||||
where
|
||||
A: Responder,
|
||||
B: Responder,
|
||||
{
|
||||
type Error = Error;
|
||||
type Future = EitherResponder<A, B>;
|
||||
|
||||
fn respond_to(self, req: &HttpRequest) -> Self::Future {
|
||||
match self {
|
||||
Either::A(a) => EitherResponder::A(a.respond_to(req)),
|
||||
Either::B(b) => EitherResponder::B(b.respond_to(req)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project(project = EitherResponderProj)]
|
||||
pub enum EitherResponder<A, B>
|
||||
where
|
||||
A: Responder,
|
||||
B: Responder,
|
||||
{
|
||||
A(#[pin] A::Future),
|
||||
B(#[pin] B::Future),
|
||||
}
|
||||
|
||||
impl<A, B> Future for EitherResponder<A, B>
|
||||
where
|
||||
A: Responder,
|
||||
B: Responder,
|
||||
{
|
||||
type Output = Result<Response, Error>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
match self.project() {
|
||||
EitherResponderProj::A(fut) => {
|
||||
Poll::Ready(ready!(fut.poll(cx)).map_err(|e| e.into()))
|
||||
}
|
||||
EitherResponderProj::B(fut) => {
|
||||
Poll::Ready(ready!(fut.poll(cx).map_err(|e| e.into())))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A composite error resulting from failure to extract an `Either<A, B>`.
|
||||
///
|
||||
/// The implementation of `Into<actix_web::Error>` will return the payload buffering error or the
|
||||
/// error from the primary extractor. To access the fallback error, use a match clause.
|
||||
#[derive(Debug)]
|
||||
pub enum EitherExtractError<A, B> {
|
||||
/// Error from payload buffering, such as exceeding payload max size limit.
|
||||
Bytes(Error),
|
||||
|
||||
/// Error from primary extractor.
|
||||
Extract(A, B),
|
||||
}
|
||||
|
||||
impl<A, B> Into<Error> for EitherExtractError<A, B>
|
||||
where
|
||||
A: Into<Error>,
|
||||
B: Into<Error>,
|
||||
{
|
||||
fn into(self) -> Error {
|
||||
match self {
|
||||
EitherExtractError::Bytes(err) => err,
|
||||
EitherExtractError::Extract(a_err, _b_err) => a_err.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides a mechanism for trying two extractors, a primary and a fallback. Useful for
|
||||
/// "polymorphic payloads" where, for example, a form might be JSON or URL encoded.
|
||||
///
|
||||
/// It is important to note that this extractor, by necessity, buffers the entire request payload
|
||||
/// as part of its implementation. Though, it does respect a `PayloadConfig`'s maximum size limit.
|
||||
impl<A, B> FromRequest for Either<A, B>
|
||||
where
|
||||
A: FromRequest + 'static,
|
||||
B: FromRequest + 'static,
|
||||
{
|
||||
type Error = EitherExtractError<A::Error, B::Error>;
|
||||
type Future = LocalBoxFuture<'static, Result<Self, Self::Error>>;
|
||||
type Config = ();
|
||||
|
||||
fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future {
|
||||
let req2 = req.clone();
|
||||
|
||||
Bytes::from_request(req, payload)
|
||||
.map_err(EitherExtractError::Bytes)
|
||||
.and_then(|bytes| bytes_to_a_or_b(req2, bytes))
|
||||
.boxed_local()
|
||||
}
|
||||
}
|
||||
|
||||
async fn bytes_to_a_or_b<A, B>(
|
||||
req: HttpRequest,
|
||||
bytes: Bytes,
|
||||
) -> Result<Either<A, B>, EitherExtractError<A::Error, B::Error>>
|
||||
where
|
||||
A: FromRequest + 'static,
|
||||
B: FromRequest + 'static,
|
||||
{
|
||||
let fallback = bytes.clone();
|
||||
let a_err;
|
||||
|
||||
let mut pl = payload_from_bytes(bytes);
|
||||
match A::from_request(&req, &mut pl).await {
|
||||
Ok(a_data) => return Ok(Either::A(a_data)),
|
||||
// store A's error for returning if B also fails
|
||||
Err(err) => a_err = err,
|
||||
};
|
||||
|
||||
let mut pl = payload_from_bytes(fallback);
|
||||
match B::from_request(&req, &mut pl).await {
|
||||
Ok(b_data) => return Ok(Either::B(b_data)),
|
||||
Err(b_err) => Err(EitherExtractError::Extract(a_err, b_err)),
|
||||
}
|
||||
}
|
||||
|
||||
fn payload_from_bytes(bytes: Bytes) -> dev::Payload {
|
||||
let (_, mut h1_payload) = actix_http::h1::Payload::create(true);
|
||||
h1_payload.unread_data(bytes);
|
||||
dev::Payload::from(h1_payload)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
test::TestRequest,
|
||||
web::{Form, Json},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct TestForm {
|
||||
hello: String,
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_either_extract_first_try() {
|
||||
let (req, mut pl) = TestRequest::default()
|
||||
.set_form(&TestForm {
|
||||
hello: "world".to_owned(),
|
||||
})
|
||||
.to_http_parts();
|
||||
|
||||
let form = Either::<Form<TestForm>, Json<TestForm>>::from_request(&req, &mut pl)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_left()
|
||||
.into_inner();
|
||||
assert_eq!(&form.hello, "world");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_either_extract_fallback() {
|
||||
let (req, mut pl) = TestRequest::default()
|
||||
.set_json(&TestForm {
|
||||
hello: "world".to_owned(),
|
||||
})
|
||||
.to_http_parts();
|
||||
|
||||
let form = Either::<Form<TestForm>, Json<TestForm>>::from_request(&req, &mut pl)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_right()
|
||||
.into_inner();
|
||||
assert_eq!(&form.hello, "world");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_either_extract_recursive_fallback() {
|
||||
let (req, mut pl) = TestRequest::default()
|
||||
.set_payload(Bytes::from_static(b"!@$%^&*()"))
|
||||
.to_http_parts();
|
||||
|
||||
let payload =
|
||||
Either::<Either<Form<TestForm>, Json<TestForm>>, Bytes>::from_request(
|
||||
&req, &mut pl,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_right();
|
||||
assert_eq!(&payload.as_ref(), &b"!@$%^&*()");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_either_extract_recursive_fallback_inner() {
|
||||
let (req, mut pl) = TestRequest::default()
|
||||
.set_json(&TestForm {
|
||||
hello: "world".to_owned(),
|
||||
})
|
||||
.to_http_parts();
|
||||
|
||||
let form =
|
||||
Either::<Either<Form<TestForm>, Json<TestForm>>, Bytes>::from_request(
|
||||
&req, &mut pl,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_left()
|
||||
.unwrap_right()
|
||||
.into_inner();
|
||||
assert_eq!(&form.hello, "world");
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
//! Helper types
|
||||
|
||||
mod either;
|
||||
pub(crate) mod form;
|
||||
pub(crate) mod json;
|
||||
mod path;
|
||||
|
@ -7,6 +8,7 @@ pub(crate) mod payload;
|
|||
mod query;
|
||||
pub(crate) mod readlines;
|
||||
|
||||
pub use self::either::{Either, EitherExtractError};
|
||||
pub use self::form::{Form, FormConfig};
|
||||
pub use self::json::{Json, JsonConfig};
|
||||
pub use self::path::{Path, PathConfig};
|
||||
|
|
|
@ -241,9 +241,10 @@ pub struct PayloadConfig {
|
|||
impl PayloadConfig {
|
||||
/// Create `PayloadConfig` instance and set max size of payload.
|
||||
pub fn new(limit: usize) -> Self {
|
||||
let mut cfg = Self::default();
|
||||
cfg.limit = limit;
|
||||
cfg
|
||||
Self {
|
||||
limit,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Change max size of payload. By default max size is 256Kb
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
# Actix http test server [](https://travis-ci.org/actix/actix-web) [](https://codecov.io/gh/actix/actix-web) [](https://crates.io/crates/actix-http-test) [](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
## Documentation & community resources
|
||||
|
||||
* [User Guide](https://actix.rs/docs/)
|
||||
* [API Documentation](https://docs.rs/actix-http-test/)
|
||||
* [Chat on gitter](https://gitter.im/actix/actix)
|
||||
* Cargo package: [actix-http-test](https://crates.io/crates/actix-http-test)
|
||||
* Minimum supported Rust version: 1.40 or later
|
Loading…
Reference in New Issue