mirror of
https://github.com/fafhrd91/actix-web
synced 2025-07-20 08:09:56 +02:00
Compare commits
29 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
9f9e0b98ad | ||
|
556646aaec | ||
|
174fb0b5f4 | ||
|
836706653b | ||
|
17f1a2b92a | ||
|
3b08b16c11 | ||
|
68eb2f26c9 | ||
|
72757887c9 | ||
|
eb5dbd43ae | ||
|
1f1dfac3f9 | ||
|
2479b14aba | ||
|
ac24703512 | ||
|
db0091ba6f | ||
|
2159158c30 | ||
|
76d790425f | ||
|
90968d4333 | ||
|
577a509875 | ||
|
a9728abfc8 | ||
|
14d1b8e2b6 | ||
|
285c73e95e | ||
|
483db7028c | ||
|
082ff46041 | ||
|
f32e8f22c8 | ||
|
766dde7c42 | ||
|
b68687044e | ||
|
c9e84e9dd3 | ||
|
0126ac46fc | ||
|
9b7ea836d0 | ||
|
537b420d35 |
@@ -31,12 +31,12 @@ before_script:
|
||||
|
||||
script:
|
||||
- |
|
||||
if [[ "$TRAVIS_RUST_VERSION" != "beta" ]]; then
|
||||
if [[ "$TRAVIS_RUST_VERSION" != "1.24.0" ]]; then
|
||||
cargo clean
|
||||
cargo test --features="alpn,tls" -- --nocapture
|
||||
fi
|
||||
- |
|
||||
if [[ "$TRAVIS_RUST_VERSION" == "beta" ]]; then
|
||||
if [[ "$TRAVIS_RUST_VERSION" == "1.24.0" ]]; then
|
||||
bash <(curl https://raw.githubusercontent.com/xd009642/tarpaulin/master/travis-install.sh)
|
||||
USE_SKEPTIC=1 cargo tarpaulin --out Xml --no-count
|
||||
bash <(curl -s https://codecov.io/bash)
|
||||
|
32
CHANGES.md
32
CHANGES.md
@@ -1,5 +1,37 @@
|
||||
# Changes
|
||||
|
||||
## [0.6.10] - 2018-05-24
|
||||
|
||||
### Added
|
||||
|
||||
* Allow to use path without traling slashes for scope registration #241
|
||||
|
||||
* Allow to set encoding for exact NamedFile #239
|
||||
|
||||
### Fixed
|
||||
|
||||
* `TestServer::post()` actually sends `GET` request #240
|
||||
|
||||
|
||||
## 0.6.9 (2018-05-22)
|
||||
|
||||
* Drop connection if request's payload is not fully consumed #236
|
||||
|
||||
* Fix streaming response with body compression
|
||||
|
||||
|
||||
## 0.6.8 (2018-05-20)
|
||||
|
||||
* Fix scope resource path extractor #234
|
||||
|
||||
* Re-use tcp listener on pause/resume
|
||||
|
||||
|
||||
## 0.6.7 (2018-05-17)
|
||||
|
||||
* Fix compilation with --no-default-features
|
||||
|
||||
|
||||
## 0.6.6 (2018-05-17)
|
||||
|
||||
* Panic during middleware execution #226
|
||||
|
@@ -1,11 +1,11 @@
|
||||
[package]
|
||||
name = "actix-web"
|
||||
version = "0.6.6"
|
||||
version = "0.6.10"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust."
|
||||
readme = "README.md"
|
||||
keywords = ["http", "web", "framework", "async", "futures"]
|
||||
homepage = "https://github.com/actix/actix-web"
|
||||
homepage = "https://actix.rs"
|
||||
repository = "https://github.com/actix/actix-web.git"
|
||||
documentation = "https://docs.rs/actix-web/"
|
||||
categories = ["network-programming", "asynchronous",
|
||||
|
14
README.md
14
README.md
@@ -2,12 +2,12 @@
|
||||
|
||||
Actix web is a simple, pragmatic and extremely fast web framework for Rust.
|
||||
|
||||
* Supported *HTTP/1.x* and [*HTTP/2.0*](https://actix.rs/book/actix-web/sec-12-http2.html) protocols
|
||||
* Supported *HTTP/1.x* and [*HTTP/2.0*](https://actix.rs/docs/http2/) protocols
|
||||
* Streaming and pipelining
|
||||
* Keep-alive and slow requests handling
|
||||
* Client/server [WebSockets](https://actix.rs/book/actix-web/sec-11-websockets.html) support
|
||||
* Client/server [WebSockets](https://actix.rs/docs/websockets/) support
|
||||
* Transparent content compression/decompression (br, gzip, deflate)
|
||||
* Configurable [request routing](https://actix.rs/book/actix-web/sec-6-url-dispatch.html)
|
||||
* Configurable [request routing](https://actix.rs/docs/url-dispatch/)
|
||||
* Graceful server shutdown
|
||||
* Multipart streams
|
||||
* Static assets
|
||||
@@ -18,12 +18,12 @@ Actix web is a simple, pragmatic and extremely fast web framework for Rust.
|
||||
[DefaultHeaders](https://actix.rs/book/actix-web/sec-9-middlewares.html#default-headers),
|
||||
[CORS](https://actix.rs/actix-web/actix_web/middleware/cors/index.html),
|
||||
[CSRF](https://actix.rs/actix-web/actix_web/middleware/csrf/index.html))
|
||||
* Includes an asynchronous [HTTP client](https://github.com/actix/actix-web/blob/master/src/client/mod.rs)
|
||||
* Includes an asynchronous [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html)
|
||||
* Built on top of [Actix actor framework](https://github.com/actix/actix)
|
||||
|
||||
## Documentation & community resources
|
||||
|
||||
* [User Guide](https://actix.rs/book/actix-web/)
|
||||
* [User Guide](https://actix.rs/docs/)
|
||||
* [API Documentation (Development)](https://actix.rs/actix-web/actix_web/)
|
||||
* [API Documentation (Releases)](https://docs.rs/actix-web/)
|
||||
* [Chat on gitter](https://gitter.im/actix/actix)
|
||||
@@ -34,9 +34,9 @@ Actix web is a simple, pragmatic and extremely fast web framework for Rust.
|
||||
|
||||
```rust
|
||||
extern crate actix_web;
|
||||
use actix_web::{http, server, App, Path};
|
||||
use actix_web::{http, server, App, Path, Responder};
|
||||
|
||||
fn index(info: Path<(u32, String)>) -> String {
|
||||
fn index(info: Path<(u32, String)>) -> impl Responder {
|
||||
format!("Hello {}! id:{}", info.1, info.0)
|
||||
}
|
||||
|
||||
|
@@ -107,16 +107,12 @@ impl<S: 'static> HttpApplication<S> {
|
||||
}
|
||||
}
|
||||
|
||||
let prefix_len = inner.prefix + prefix_len - 1;
|
||||
let prefix_len = inner.prefix + prefix_len;
|
||||
let path: &'static str =
|
||||
unsafe { &*(&req.path()[prefix_len..] as *const _) };
|
||||
|
||||
req.set_prefix_len(prefix_len as u16);
|
||||
if path.is_empty() {
|
||||
req.match_info_mut().set("tail", "/");
|
||||
} else {
|
||||
req.match_info_mut().set("tail", path);
|
||||
}
|
||||
req.match_info_mut().set("tail", path);
|
||||
return HandlerType::Handler(idx);
|
||||
}
|
||||
}
|
||||
@@ -369,14 +365,6 @@ where
|
||||
{
|
||||
{
|
||||
let mut scope = Box::new(f(Scope::new()));
|
||||
|
||||
let mut path = path.trim().trim_right_matches('/').to_owned();
|
||||
if !path.is_empty() && !path.starts_with('/') {
|
||||
path.insert(0, '/')
|
||||
}
|
||||
if !path.ends_with('/') {
|
||||
path.push('/');
|
||||
}
|
||||
let parts = self.parts.as_mut().expect("Use after finish");
|
||||
|
||||
let filters = scope.take_filters();
|
||||
|
164
src/fs.rs
164
src/fs.rs
@@ -20,7 +20,7 @@ use mime_guess::{get_mime_type, guess_mime_type};
|
||||
use error::Error;
|
||||
use handler::{AsyncResult, Handler, Responder, RouteHandler, WrapHandler};
|
||||
use header;
|
||||
use http::{HttpRange, Method, StatusCode};
|
||||
use http::{ContentEncoding, HttpRange, Method, StatusCode};
|
||||
use httpmessage::HttpMessage;
|
||||
use httprequest::HttpRequest;
|
||||
use httpresponse::HttpResponse;
|
||||
@@ -38,6 +38,7 @@ pub struct NamedFile {
|
||||
md: Metadata,
|
||||
modified: Option<SystemTime>,
|
||||
cpu_pool: Option<CpuPool>,
|
||||
encoding: Option<ContentEncoding>,
|
||||
only_get: bool,
|
||||
status_code: StatusCode,
|
||||
}
|
||||
@@ -58,12 +59,14 @@ impl NamedFile {
|
||||
let path = path.as_ref().to_path_buf();
|
||||
let modified = md.modified().ok();
|
||||
let cpu_pool = None;
|
||||
let encoding = None;
|
||||
Ok(NamedFile {
|
||||
path,
|
||||
file,
|
||||
md,
|
||||
modified,
|
||||
cpu_pool,
|
||||
encoding,
|
||||
only_get: false,
|
||||
status_code: StatusCode::OK,
|
||||
})
|
||||
@@ -114,6 +117,13 @@ impl NamedFile {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set content encoding for serving this file
|
||||
#[inline]
|
||||
pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self {
|
||||
self.encoding = Some(enc);
|
||||
self
|
||||
}
|
||||
|
||||
fn etag(&self) -> Option<header::EntityTag> {
|
||||
// This etag format is similar to Apache's.
|
||||
self.modified.as_ref().map(|mtime| {
|
||||
@@ -219,6 +229,9 @@ impl Responder for NamedFile {
|
||||
),
|
||||
);
|
||||
});
|
||||
if let Some(current_encoding) = self.encoding {
|
||||
resp.content_encoding(current_encoding);
|
||||
}
|
||||
let reader = ChunkedReadFile {
|
||||
size: self.md.len(),
|
||||
offset: 0,
|
||||
@@ -264,6 +277,9 @@ impl Responder for NamedFile {
|
||||
};
|
||||
|
||||
let mut resp = HttpResponse::build(self.status_code);
|
||||
if let Some(current_encoding) = self.encoding {
|
||||
resp.content_encoding(current_encoding);
|
||||
}
|
||||
|
||||
resp.if_some(self.path().extension(), |ext, resp| {
|
||||
resp.set(header::ContentType(get_mime_type(&ext.to_string_lossy())));
|
||||
@@ -289,30 +305,29 @@ impl Responder for NamedFile {
|
||||
resp.set(header::ETag(etag));
|
||||
});
|
||||
|
||||
// TODO: Debug, enabling "accept-ranges: bytes" causes problems with
|
||||
// certain clients when not using the ranges header.
|
||||
//resp.header(header::ACCEPT_RANGES, format!("bytes"));
|
||||
resp.header(header::ACCEPT_RANGES, "bytes");
|
||||
|
||||
let mut length = self.md.len();
|
||||
let mut offset = 0;
|
||||
|
||||
// check for ranges header
|
||||
// check for range header
|
||||
if let Some(ranges) = req.headers().get(header::RANGE) {
|
||||
if let Ok(rangesheader) = ranges.to_str() {
|
||||
if let Ok(rangesvec) = HttpRange::parse(rangesheader, length) {
|
||||
length = rangesvec[0].length - 1;
|
||||
length = rangesvec[0].length;
|
||||
offset = rangesvec[0].start;
|
||||
resp.content_encoding(ContentEncoding::Identity);
|
||||
resp.header(
|
||||
header::RANGE,
|
||||
header::CONTENT_RANGE,
|
||||
format!(
|
||||
"bytes={}-{}/{}",
|
||||
"bytes {}-{}/{}",
|
||||
offset,
|
||||
offset + length,
|
||||
offset + length - 1,
|
||||
self.md.len()
|
||||
),
|
||||
);
|
||||
} else {
|
||||
resp.header(header::RANGE, format!("*/{}", length));
|
||||
resp.header(header::CONTENT_RANGE, format!("bytes */{}", length));
|
||||
return Ok(resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish());
|
||||
};
|
||||
} else {
|
||||
@@ -778,6 +793,7 @@ mod tests {
|
||||
App::new().handler("test", StaticFiles::new(".").index_file("Cargo.toml"))
|
||||
});
|
||||
|
||||
// Valid range header
|
||||
let request = srv
|
||||
.get()
|
||||
.uri(srv.url("/t%65st/Cargo.toml"))
|
||||
@@ -787,10 +803,21 @@ mod tests {
|
||||
let response = srv.execute(request.send()).unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
|
||||
|
||||
// Invalid range header
|
||||
let request = srv
|
||||
.get()
|
||||
.uri(srv.url("/t%65st/Cargo.toml"))
|
||||
.header(header::RANGE, "bytes=1-0")
|
||||
.finish()
|
||||
.unwrap();
|
||||
let response = srv.execute(request.send()).unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_named_file_ranges_headers() {
|
||||
fn test_named_file_content_range_headers() {
|
||||
let mut srv = test::TestServer::with_factory(|| {
|
||||
App::new().handler(
|
||||
"test",
|
||||
@@ -798,13 +825,64 @@ mod tests {
|
||||
)
|
||||
});
|
||||
|
||||
// Valid range header
|
||||
let request = srv
|
||||
.get()
|
||||
.uri(srv.url("/t%65st/tests/test.binary"))
|
||||
.header(header::RANGE, "bytes=10-20")
|
||||
.finish()
|
||||
.unwrap();
|
||||
|
||||
let response = srv.execute(request.send()).unwrap();
|
||||
|
||||
let contentrange = response
|
||||
.headers()
|
||||
.get(header::CONTENT_RANGE)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(contentrange, "bytes 10-20/100");
|
||||
|
||||
// Invalid range header
|
||||
let request = srv
|
||||
.get()
|
||||
.uri(srv.url("/t%65st/tests/test.binary"))
|
||||
.header(header::RANGE, "bytes=10-5")
|
||||
.finish()
|
||||
.unwrap();
|
||||
|
||||
let response = srv.execute(request.send()).unwrap();
|
||||
|
||||
let contentrange = response
|
||||
.headers()
|
||||
.get(header::CONTENT_RANGE)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(contentrange, "bytes */100");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_named_file_content_length_headers() {
|
||||
let mut srv = test::TestServer::with_factory(|| {
|
||||
App::new().handler(
|
||||
"test",
|
||||
StaticFiles::new(".").index_file("tests/test.binary"),
|
||||
)
|
||||
});
|
||||
|
||||
// Valid range header
|
||||
let request = srv
|
||||
.get()
|
||||
.uri(srv.url("/t%65st/tests/test.binary"))
|
||||
.header(header::RANGE, "bytes=10-20")
|
||||
.finish()
|
||||
.unwrap();
|
||||
|
||||
let response = srv.execute(request.send()).unwrap();
|
||||
|
||||
let contentlength = response
|
||||
.headers()
|
||||
.get(header::CONTENT_LENGTH)
|
||||
@@ -812,23 +890,62 @@ mod tests {
|
||||
.to_str()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(contentlength, "10");
|
||||
assert_eq!(contentlength, "11");
|
||||
|
||||
// Invalid range header
|
||||
let request = srv
|
||||
.get()
|
||||
.uri(srv.url("/t%65st/tests/test.binary"))
|
||||
.header(header::RANGE, "bytes=10-20")
|
||||
.header(header::RANGE, "bytes=10-8")
|
||||
.finish()
|
||||
.unwrap();
|
||||
|
||||
let response = srv.execute(request.send()).unwrap();
|
||||
let range = response
|
||||
|
||||
let contentlength = response
|
||||
.headers()
|
||||
.get(header::RANGE)
|
||||
.get(header::CONTENT_LENGTH)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(range, "bytes=10-20/100");
|
||||
assert_eq!(contentlength, "0");
|
||||
|
||||
// Without range header
|
||||
let request = srv
|
||||
.get()
|
||||
.uri(srv.url("/t%65st/tests/test.binary"))
|
||||
.no_default_headers()
|
||||
.finish()
|
||||
.unwrap();
|
||||
|
||||
let response = srv.execute(request.send()).unwrap();
|
||||
|
||||
let contentlength = response
|
||||
.headers()
|
||||
.get(header::CONTENT_LENGTH)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(contentlength, "100");
|
||||
|
||||
// chunked
|
||||
let request = srv
|
||||
.get()
|
||||
.uri(srv.url("/t%65st/tests/test.binary"))
|
||||
.finish()
|
||||
.unwrap();
|
||||
|
||||
let response = srv.execute(request.send()).unwrap();
|
||||
|
||||
let te = response
|
||||
.headers()
|
||||
.get(header::TRANSFER_ENCODING)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap();
|
||||
assert_eq!(te, "chunked");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -840,6 +957,21 @@ mod tests {
|
||||
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_named_file_content_encoding() {
|
||||
let req = TestRequest::default().method(Method::GET).finish();
|
||||
let file = NamedFile::open("Cargo.toml").unwrap();
|
||||
|
||||
assert!(file.encoding.is_none());
|
||||
let resp = file
|
||||
.set_content_encoding(ContentEncoding::Identity)
|
||||
.respond_to(&req)
|
||||
.unwrap();
|
||||
|
||||
assert!(resp.content_encoding().is_some());
|
||||
assert_eq!(resp.content_encoding().unwrap().as_str(), "identity");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_named_file_any_method() {
|
||||
let req = TestRequest::default().method(Method::POST).finish();
|
||||
|
@@ -25,7 +25,7 @@
|
||||
//! Besides the API documentation (which you are currently looking
|
||||
//! at!), several other resources are available:
|
||||
//!
|
||||
//! * [User Guide](https://actix.rs/book/actix-web/)
|
||||
//! * [User Guide](https://actix.rs/docs/)
|
||||
//! * [Chat on gitter](https://gitter.im/actix/actix)
|
||||
//! * [GitHub repository](https://github.com/actix/actix-web)
|
||||
//! * [Cargo package](https://crates.io/crates/actix-web)
|
||||
|
@@ -58,6 +58,15 @@ impl<'a> Params<'a> {
|
||||
self.0.push((name, value));
|
||||
}
|
||||
|
||||
pub(crate) fn remove(&mut self, name: &str) {
|
||||
for idx in (0..self.0.len()).rev() {
|
||||
if self.0[idx].0 == name {
|
||||
self.0.remove(idx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if there are any matched patterns
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
|
@@ -311,8 +311,16 @@ impl Resource {
|
||||
None
|
||||
}
|
||||
}
|
||||
PatternType::Prefix(ref s) => if path.starts_with(s) {
|
||||
PatternType::Prefix(ref s) => if path == s {
|
||||
Some(s.len())
|
||||
} else if path.starts_with(s)
|
||||
&& (s.ends_with('/') || path.split_at(s.len()).1.starts_with('/'))
|
||||
{
|
||||
if s.ends_with('/') {
|
||||
Some(s.len() - 1)
|
||||
} else {
|
||||
Some(s.len())
|
||||
}
|
||||
} else {
|
||||
None
|
||||
},
|
||||
|
179
src/scope.rs
179
src/scope.rs
@@ -137,14 +137,6 @@ impl<S: 'static> Scope<S> {
|
||||
};
|
||||
let mut scope = f(scope);
|
||||
|
||||
let mut path = path.trim().trim_right_matches('/').to_owned();
|
||||
if !path.is_empty() && !path.starts_with('/') {
|
||||
path.insert(0, '/')
|
||||
}
|
||||
if !path.ends_with('/') {
|
||||
path.push('/');
|
||||
}
|
||||
|
||||
let state = Rc::new(state);
|
||||
let filters: Vec<Box<Predicate<S>>> = vec![Box::new(FiltersWrapper {
|
||||
state: Rc::clone(&state),
|
||||
@@ -191,14 +183,6 @@ impl<S: 'static> Scope<S> {
|
||||
};
|
||||
let mut scope = f(scope);
|
||||
|
||||
let mut path = path.trim().trim_right_matches('/').to_owned();
|
||||
if !path.is_empty() && !path.starts_with('/') {
|
||||
path.insert(0, '/')
|
||||
}
|
||||
if !path.ends_with('/') {
|
||||
path.push('/');
|
||||
}
|
||||
|
||||
let filters = scope.take_filters();
|
||||
self.nested.push((
|
||||
Resource::prefix("", &path),
|
||||
@@ -253,7 +237,12 @@ impl<S: 'static> Scope<S> {
|
||||
|
||||
let mut handler = ResourceHandler::default();
|
||||
handler.method(method).with(f);
|
||||
let pattern = Resource::new(handler.get_name(), path);
|
||||
let pattern = Resource::with_prefix(
|
||||
handler.get_name(),
|
||||
path,
|
||||
if path.is_empty() { "" } else { "/" },
|
||||
false,
|
||||
);
|
||||
Rc::get_mut(&mut self.resources)
|
||||
.expect("Can not use after configuration")
|
||||
.push((pattern, Rc::new(UnsafeCell::new(handler))));
|
||||
@@ -293,7 +282,12 @@ impl<S: 'static> Scope<S> {
|
||||
let mut handler = ResourceHandler::default();
|
||||
f(&mut handler);
|
||||
|
||||
let pattern = Resource::new(handler.get_name(), path);
|
||||
let pattern = Resource::with_prefix(
|
||||
handler.get_name(),
|
||||
path,
|
||||
if path.is_empty() { "" } else { "/" },
|
||||
false,
|
||||
);
|
||||
Rc::get_mut(&mut self.resources)
|
||||
.expect("Can not use after configuration")
|
||||
.push((pattern, Rc::new(UnsafeCell::new(handler))));
|
||||
@@ -329,13 +323,13 @@ impl<S: 'static> Scope<S> {
|
||||
impl<S: 'static> RouteHandler<S> for Scope<S> {
|
||||
fn handle(&mut self, mut req: HttpRequest<S>) -> AsyncResult<HttpResponse> {
|
||||
let path = unsafe { &*(&req.match_info()["tail"] as *const _) };
|
||||
let path = if path == "" { "/" } else { path };
|
||||
|
||||
// recognize resources
|
||||
for &(ref pattern, ref resource) in self.resources.iter() {
|
||||
if pattern.match_with_params(path, req.match_info_mut()) {
|
||||
let default = unsafe { &mut *self.default.as_ref().get() };
|
||||
|
||||
req.match_info_mut().remove("tail");
|
||||
if self.middlewares.is_empty() {
|
||||
let resource = unsafe { &mut *resource.get() };
|
||||
return resource.handle(req, Some(default));
|
||||
@@ -363,16 +357,12 @@ impl<S: 'static> RouteHandler<S> for Scope<S> {
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
let prefix_len = len + prefix_len - 1;
|
||||
let prefix_len = len + prefix_len;
|
||||
let path: &'static str =
|
||||
unsafe { &*(&req.path()[prefix_len..] as *const _) };
|
||||
|
||||
req.set_prefix_len(prefix_len as u16);
|
||||
if path.is_empty() {
|
||||
req.match_info_mut().set("tail", "/");
|
||||
} else {
|
||||
req.match_info_mut().set("tail", path);
|
||||
}
|
||||
req.match_info_mut().set("tail", path);
|
||||
|
||||
let hnd: &mut RouteHandler<_> =
|
||||
unsafe { (&mut *(handler.get())).as_mut() };
|
||||
@@ -783,6 +773,59 @@ mod tests {
|
||||
assert_eq!(resp.as_msg().status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scope_root() {
|
||||
let mut app = App::new()
|
||||
.scope("/app", |scope| {
|
||||
scope
|
||||
.resource("", |r| r.f(|_| HttpResponse::Ok()))
|
||||
.resource("/", |r| r.f(|_| HttpResponse::Created()))
|
||||
})
|
||||
.finish();
|
||||
|
||||
let req = TestRequest::with_uri("/app").finish();
|
||||
let resp = app.run(req);
|
||||
assert_eq!(resp.as_msg().status(), StatusCode::OK);
|
||||
|
||||
let req = TestRequest::with_uri("/app/").finish();
|
||||
let resp = app.run(req);
|
||||
assert_eq!(resp.as_msg().status(), StatusCode::CREATED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scope_root2() {
|
||||
let mut app = App::new()
|
||||
.scope("/app/", |scope| {
|
||||
scope.resource("", |r| r.f(|_| HttpResponse::Ok()))
|
||||
})
|
||||
.finish();
|
||||
|
||||
let req = TestRequest::with_uri("/app").finish();
|
||||
let resp = app.run(req);
|
||||
assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND);
|
||||
|
||||
let req = TestRequest::with_uri("/app/").finish();
|
||||
let resp = app.run(req);
|
||||
assert_eq!(resp.as_msg().status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scope_root3() {
|
||||
let mut app = App::new()
|
||||
.scope("/app/", |scope| {
|
||||
scope.resource("/", |r| r.f(|_| HttpResponse::Ok()))
|
||||
})
|
||||
.finish();
|
||||
|
||||
let req = TestRequest::with_uri("/app").finish();
|
||||
let resp = app.run(req);
|
||||
assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND);
|
||||
|
||||
let req = TestRequest::with_uri("/app/").finish();
|
||||
let resp = app.run(req);
|
||||
assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scope_route() {
|
||||
let mut app = App::new()
|
||||
@@ -884,6 +927,71 @@ mod tests {
|
||||
assert_eq!(resp.as_msg().status(), StatusCode::CREATED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scope_with_state_root() {
|
||||
struct State;
|
||||
|
||||
let mut app = App::new()
|
||||
.scope("/app", |scope| {
|
||||
scope.with_state("/t1", State, |scope| {
|
||||
scope
|
||||
.resource("", |r| r.f(|_| HttpResponse::Ok()))
|
||||
.resource("/", |r| r.f(|_| HttpResponse::Created()))
|
||||
})
|
||||
})
|
||||
.finish();
|
||||
|
||||
let req = TestRequest::with_uri("/app/t1").finish();
|
||||
let resp = app.run(req);
|
||||
assert_eq!(resp.as_msg().status(), StatusCode::OK);
|
||||
|
||||
let req = TestRequest::with_uri("/app/t1/").finish();
|
||||
let resp = app.run(req);
|
||||
assert_eq!(resp.as_msg().status(), StatusCode::CREATED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scope_with_state_root2() {
|
||||
struct State;
|
||||
|
||||
let mut app = App::new()
|
||||
.scope("/app", |scope| {
|
||||
scope.with_state("/t1/", State, |scope| {
|
||||
scope.resource("", |r| r.f(|_| HttpResponse::Ok()))
|
||||
})
|
||||
})
|
||||
.finish();
|
||||
|
||||
let req = TestRequest::with_uri("/app/t1").finish();
|
||||
let resp = app.run(req);
|
||||
assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND);
|
||||
|
||||
let req = TestRequest::with_uri("/app/t1/").finish();
|
||||
let resp = app.run(req);
|
||||
assert_eq!(resp.as_msg().status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scope_with_state_root3() {
|
||||
struct State;
|
||||
|
||||
let mut app = App::new()
|
||||
.scope("/app", |scope| {
|
||||
scope.with_state("/t1/", State, |scope| {
|
||||
scope.resource("/", |r| r.f(|_| HttpResponse::Ok()))
|
||||
})
|
||||
})
|
||||
.finish();
|
||||
|
||||
let req = TestRequest::with_uri("/app/t1").finish();
|
||||
let resp = app.run(req);
|
||||
assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND);
|
||||
|
||||
let req = TestRequest::with_uri("/app/t1/").finish();
|
||||
let resp = app.run(req);
|
||||
assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scope_with_state_filter() {
|
||||
struct State;
|
||||
@@ -926,6 +1034,27 @@ mod tests {
|
||||
assert_eq!(resp.as_msg().status(), StatusCode::CREATED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_scope_root() {
|
||||
let mut app = App::new()
|
||||
.scope("/app", |scope| {
|
||||
scope.nested("/t1", |scope| {
|
||||
scope
|
||||
.resource("", |r| r.f(|_| HttpResponse::Ok()))
|
||||
.resource("/", |r| r.f(|_| HttpResponse::Created()))
|
||||
})
|
||||
})
|
||||
.finish();
|
||||
|
||||
let req = TestRequest::with_uri("/app/t1").finish();
|
||||
let resp = app.run(req);
|
||||
assert_eq!(resp.as_msg().status(), StatusCode::OK);
|
||||
|
||||
let req = TestRequest::with_uri("/app/t1/").finish();
|
||||
let resp = app.run(req);
|
||||
assert_eq!(resp.as_msg().status(), StatusCode::CREATED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_scope_filter() {
|
||||
let mut app = App::new()
|
||||
|
@@ -33,6 +33,7 @@ pub(crate) enum PayloadType {
|
||||
}
|
||||
|
||||
impl PayloadType {
|
||||
#[cfg(any(feature = "brotli", feature = "flate2"))]
|
||||
pub fn new(headers: &HeaderMap, sender: PayloadSender) -> PayloadType {
|
||||
// check content-encoding
|
||||
let enc = if let Some(enc) = headers.get(CONTENT_ENCODING) {
|
||||
@@ -52,6 +53,11 @@ impl PayloadType {
|
||||
_ => PayloadType::Encoding(Box::new(EncodedPayload::new(sender, enc))),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "brotli", feature = "flate2")))]
|
||||
pub fn new(headers: &HeaderMap, sender: PayloadSender) -> PayloadType {
|
||||
PayloadType::Sender(sender)
|
||||
}
|
||||
}
|
||||
|
||||
impl PayloadWriter for PayloadType {
|
||||
@@ -399,6 +405,7 @@ impl ContentEncoder {
|
||||
|
||||
// Enable content encoding only if response does not contain Content-Encoding
|
||||
// header
|
||||
#[cfg(any(feature = "brotli", feature = "flate2"))]
|
||||
let mut encoding = if has_body {
|
||||
let encoding = match response_encoding {
|
||||
ContentEncoding::Auto => {
|
||||
@@ -425,6 +432,8 @@ impl ContentEncoder {
|
||||
} else {
|
||||
ContentEncoding::Identity
|
||||
};
|
||||
#[cfg(not(any(feature = "brotli", feature = "flate2")))]
|
||||
let mut encoding = ContentEncoding::Identity;
|
||||
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(match_ref_pats))]
|
||||
let mut transfer = match resp.body() {
|
||||
@@ -435,40 +444,42 @@ impl ContentEncoder {
|
||||
TransferEncoding::length(0, buf)
|
||||
}
|
||||
&Body::Binary(_) => {
|
||||
if !(encoding == ContentEncoding::Identity
|
||||
|| encoding == ContentEncoding::Auto)
|
||||
#[cfg(any(feature = "brotli", feature = "flate2"))]
|
||||
{
|
||||
let tmp = SharedBytes::default();
|
||||
let transfer = TransferEncoding::eof(tmp.clone());
|
||||
let mut enc = match encoding {
|
||||
#[cfg(feature = "flate2")]
|
||||
ContentEncoding::Deflate => ContentEncoder::Deflate(
|
||||
DeflateEncoder::new(transfer, Compression::fast()),
|
||||
),
|
||||
#[cfg(feature = "flate2")]
|
||||
ContentEncoding::Gzip => ContentEncoder::Gzip(GzEncoder::new(
|
||||
transfer,
|
||||
Compression::fast(),
|
||||
)),
|
||||
#[cfg(feature = "brotli")]
|
||||
ContentEncoding::Br => {
|
||||
ContentEncoder::Br(BrotliEncoder::new(transfer, 3))
|
||||
}
|
||||
ContentEncoding::Identity | ContentEncoding::Auto => {
|
||||
unreachable!()
|
||||
}
|
||||
};
|
||||
if !(encoding == ContentEncoding::Identity
|
||||
|| encoding == ContentEncoding::Auto)
|
||||
{
|
||||
let tmp = SharedBytes::default();
|
||||
let transfer = TransferEncoding::eof(tmp.clone());
|
||||
let mut enc = match encoding {
|
||||
#[cfg(feature = "flate2")]
|
||||
ContentEncoding::Deflate => ContentEncoder::Deflate(
|
||||
DeflateEncoder::new(transfer, Compression::fast()),
|
||||
),
|
||||
#[cfg(feature = "flate2")]
|
||||
ContentEncoding::Gzip => ContentEncoder::Gzip(
|
||||
GzEncoder::new(transfer, Compression::fast()),
|
||||
),
|
||||
#[cfg(feature = "brotli")]
|
||||
ContentEncoding::Br => {
|
||||
ContentEncoder::Br(BrotliEncoder::new(transfer, 3))
|
||||
}
|
||||
ContentEncoding::Identity | ContentEncoding::Auto => {
|
||||
unreachable!()
|
||||
}
|
||||
};
|
||||
|
||||
let bin = resp.replace_body(Body::Empty).binary();
|
||||
let bin = resp.replace_body(Body::Empty).binary();
|
||||
|
||||
// TODO return error!
|
||||
let _ = enc.write(bin);
|
||||
let _ = enc.write_eof();
|
||||
let body = tmp.take();
|
||||
len = body.len();
|
||||
// TODO return error!
|
||||
let _ = enc.write(bin);
|
||||
let _ = enc.write_eof();
|
||||
let body = tmp.take();
|
||||
len = body.len();
|
||||
|
||||
encoding = ContentEncoding::Identity;
|
||||
resp.replace_body(Binary::from(body));
|
||||
encoding = ContentEncoding::Identity;
|
||||
resp.replace_body(Binary::from(body));
|
||||
}
|
||||
}
|
||||
|
||||
if is_head {
|
||||
@@ -494,6 +505,11 @@ impl ContentEncoder {
|
||||
}
|
||||
TransferEncoding::eof(buf)
|
||||
} else {
|
||||
if !(encoding == ContentEncoding::Identity
|
||||
|| encoding == ContentEncoding::Auto)
|
||||
{
|
||||
resp.headers_mut().remove(CONTENT_LENGTH);
|
||||
}
|
||||
ContentEncoder::streaming_encoding(buf, version, resp)
|
||||
}
|
||||
}
|
||||
|
@@ -270,7 +270,12 @@ where
|
||||
debug!("Error sending data: {}", err);
|
||||
return Err(());
|
||||
}
|
||||
_ => (),
|
||||
Ok(Async::Ready(_)) => {
|
||||
// non consumed payload in that case close connection
|
||||
if self.payload.is_some() && self.tasks.is_empty() {
|
||||
return Ok(Async::Ready(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
#![cfg_attr(feature = "cargo-clippy", allow(redundant_field_names))]
|
||||
|
||||
use bytes::BufMut;
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use futures::{Async, Poll};
|
||||
use std::io;
|
||||
use std::rc::Rc;
|
||||
@@ -45,7 +45,7 @@ impl<T: AsyncWrite, H: 'static> H1Writer<T, H> {
|
||||
stream: T, buf: SharedBytes, settings: Rc<WorkerSettings<H>>,
|
||||
) -> H1Writer<T, H> {
|
||||
H1Writer {
|
||||
flags: Flags::empty(),
|
||||
flags: Flags::KEEPALIVE,
|
||||
encoder: ContentEncoder::empty(buf.clone()),
|
||||
written: 0,
|
||||
headers_size: 0,
|
||||
@@ -62,7 +62,7 @@ impl<T: AsyncWrite, H: 'static> H1Writer<T, H> {
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.written = 0;
|
||||
self.flags = Flags::empty();
|
||||
self.flags = Flags::KEEPALIVE;
|
||||
}
|
||||
|
||||
pub fn disconnected(&mut self) {
|
||||
@@ -100,6 +100,16 @@ impl<T: AsyncWrite, H: 'static> Writer for H1Writer<T, H> {
|
||||
self.written
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_date(&self, dst: &mut BytesMut) {
|
||||
self.settings.set_date(dst)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn buffer(&self) -> &mut BytesMut {
|
||||
self.buffer.get_mut()
|
||||
}
|
||||
|
||||
fn start(
|
||||
&mut self, req: &mut HttpInnerMessage, msg: &mut HttpResponse,
|
||||
encoding: ContentEncoding,
|
||||
@@ -108,9 +118,9 @@ impl<T: AsyncWrite, H: 'static> Writer for H1Writer<T, H> {
|
||||
self.encoder =
|
||||
ContentEncoder::for_server(self.buffer.clone(), req, msg, encoding);
|
||||
if msg.keep_alive().unwrap_or_else(|| req.keep_alive()) {
|
||||
self.flags.insert(Flags::STARTED | Flags::KEEPALIVE);
|
||||
self.flags = Flags::STARTED | Flags::KEEPALIVE;
|
||||
} else {
|
||||
self.flags.insert(Flags::STARTED);
|
||||
self.flags = Flags::STARTED;
|
||||
}
|
||||
|
||||
// Connection upgrade
|
||||
|
@@ -71,6 +71,16 @@ impl<H: 'static> Writer for H2Writer<H> {
|
||||
self.written
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_date(&self, dst: &mut BytesMut) {
|
||||
self.settings.set_date(dst)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn buffer(&self) -> &mut BytesMut {
|
||||
self.buffer.get_mut()
|
||||
}
|
||||
|
||||
fn start(
|
||||
&mut self, req: &mut HttpInnerMessage, msg: &mut HttpResponse,
|
||||
encoding: ContentEncoding,
|
||||
|
@@ -143,7 +143,7 @@ pub(crate) fn write_status_line(version: Version, mut n: u16, bytes: &mut BytesM
|
||||
}
|
||||
|
||||
/// NOTE: bytes object has to contain enough space
|
||||
pub(crate) fn write_content_length(mut n: usize, bytes: &mut BytesMut) {
|
||||
pub fn write_content_length(mut n: usize, bytes: &mut BytesMut) {
|
||||
if n < 10 {
|
||||
let mut buf: [u8; 21] = [
|
||||
b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l', b'e',
|
||||
|
@@ -3,7 +3,8 @@ use std::net::Shutdown;
|
||||
use std::{io, time};
|
||||
|
||||
use actix;
|
||||
use futures::Poll;
|
||||
use bytes::BytesMut;
|
||||
use futures::{Async, Poll};
|
||||
use tokio_core::net::TcpStream;
|
||||
use tokio_io::{AsyncRead, AsyncWrite};
|
||||
|
||||
@@ -24,6 +25,9 @@ mod worker;
|
||||
pub use self::settings::ServerSettings;
|
||||
pub use self::srv::HttpServer;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub use self::helpers::write_content_length;
|
||||
|
||||
use body::Binary;
|
||||
use error::Error;
|
||||
use header::ContentEncoding;
|
||||
@@ -132,13 +136,15 @@ impl HttpHandler for Box<HttpHandler> {
|
||||
#[doc(hidden)]
|
||||
pub trait HttpHandlerTask {
|
||||
/// Poll task, this method is used before or after *io* object is available
|
||||
fn poll(&mut self) -> Poll<(), Error>;
|
||||
fn poll(&mut self) -> Poll<(), Error> {
|
||||
Ok(Async::Ready(()))
|
||||
}
|
||||
|
||||
/// Poll task when *io* object is available
|
||||
fn poll_io(&mut self, io: &mut Writer) -> Poll<bool, Error>;
|
||||
|
||||
/// Connection is disconnected
|
||||
fn disconnected(&mut self);
|
||||
fn disconnected(&mut self) {}
|
||||
}
|
||||
|
||||
/// Conversion helper trait
|
||||
@@ -168,8 +174,16 @@ pub enum WriterState {
|
||||
#[doc(hidden)]
|
||||
/// Stream writer
|
||||
pub trait Writer {
|
||||
/// number of bytes written to the stream
|
||||
fn written(&self) -> u64;
|
||||
|
||||
#[doc(hidden)]
|
||||
fn set_date(&self, st: &mut BytesMut);
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(mut_from_ref))]
|
||||
fn buffer(&self) -> &mut BytesMut;
|
||||
|
||||
fn start(
|
||||
&mut self, req: &mut HttpInnerMessage, resp: &mut HttpResponse,
|
||||
encoding: ContentEncoding,
|
||||
|
@@ -309,7 +309,7 @@ where
|
||||
|
||||
/// The socket address to bind
|
||||
///
|
||||
/// To mind multiple addresses this method can be call multiple times.
|
||||
/// To bind multiple addresses this method can be called multiple times.
|
||||
pub fn bind<S: net::ToSocketAddrs>(mut self, addr: S) -> io::Result<Self> {
|
||||
let sockets = self.bind2(addr)?;
|
||||
self.sockets.extend(sockets);
|
||||
@@ -319,7 +319,7 @@ where
|
||||
#[cfg(feature = "tls")]
|
||||
/// The ssl socket address to bind
|
||||
///
|
||||
/// To mind multiple addresses this method can be call multiple times.
|
||||
/// To bind multiple addresses this method can be called multiple times.
|
||||
pub fn bind_tls<S: net::ToSocketAddrs>(
|
||||
mut self, addr: S, acceptor: TlsAcceptor,
|
||||
) -> io::Result<Self> {
|
||||
@@ -450,7 +450,6 @@ impl<H: IntoHttpHandler> HttpServer<H> {
|
||||
self.accept.push(start_accept_thread(
|
||||
token,
|
||||
sock,
|
||||
self.backlog,
|
||||
tx.clone(),
|
||||
socks.clone(),
|
||||
workers.clone(),
|
||||
@@ -782,7 +781,7 @@ enum Command {
|
||||
}
|
||||
|
||||
fn start_accept_thread(
|
||||
token: usize, sock: Socket, backlog: i32, srv: mpsc::UnboundedSender<ServerCommand>,
|
||||
token: usize, sock: Socket, srv: mpsc::UnboundedSender<ServerCommand>,
|
||||
socks: Slab<SocketInfo>,
|
||||
mut workers: Vec<(usize, mpsc::UnboundedSender<Conn<net::TcpStream>>)>,
|
||||
) -> (mio::SetReadiness, sync_mpsc::Sender<Command>) {
|
||||
@@ -892,8 +891,8 @@ fn start_accept_thread(
|
||||
},
|
||||
CMD => match rx.try_recv() {
|
||||
Ok(cmd) => match cmd {
|
||||
Command::Pause => if let Some(server) = server.take() {
|
||||
if let Err(err) = poll.deregister(&server) {
|
||||
Command::Pause => if let Some(ref server) = server {
|
||||
if let Err(err) = poll.deregister(server) {
|
||||
error!(
|
||||
"Can not deregister server socket {}",
|
||||
err
|
||||
@@ -906,15 +905,6 @@ fn start_accept_thread(
|
||||
}
|
||||
},
|
||||
Command::Resume => {
|
||||
let lst = create_tcp_listener(addr, backlog)
|
||||
.expect("Can not create net::TcpListener");
|
||||
|
||||
server = Some(
|
||||
mio::net::TcpListener::from_std(lst).expect(
|
||||
"Can not create mio::net::TcpListener",
|
||||
),
|
||||
);
|
||||
|
||||
if let Some(ref server) = server {
|
||||
if let Err(err) = poll.register(
|
||||
server,
|
||||
|
@@ -239,9 +239,9 @@ impl StreamHandlerType {
|
||||
match *self {
|
||||
StreamHandlerType::Normal => "http",
|
||||
#[cfg(feature = "tls")]
|
||||
StreamHandlerType::Tls(ref acceptor) => "https",
|
||||
StreamHandlerType::Tls(_) => "https",
|
||||
#[cfg(feature = "alpn")]
|
||||
StreamHandlerType::Alpn(ref acceptor) => "https",
|
||||
StreamHandlerType::Alpn(_) => "https",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -217,7 +217,7 @@ impl TestServer {
|
||||
|
||||
/// Create `POST` request
|
||||
pub fn post(&self) -> ClientRequestBuilder {
|
||||
ClientRequest::get(self.url("/").as_str())
|
||||
ClientRequest::post(self.url("/").as_str())
|
||||
}
|
||||
|
||||
/// Create `HEAD` request
|
||||
|
@@ -368,6 +368,77 @@ fn test_path_and_query_extractor2_async4() {
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scope_and_path_extractor() {
|
||||
let mut srv = test::TestServer::with_factory(move || {
|
||||
App::new().scope("/sc", |scope| {
|
||||
scope.resource("/{num}/index.html", |r| {
|
||||
r.route()
|
||||
.with(|p: Path<(usize,)>| format!("Welcome {}!", p.0))
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
// client request
|
||||
let request = srv
|
||||
.get()
|
||||
.uri(srv.url("/sc/10/index.html"))
|
||||
.finish()
|
||||
.unwrap();
|
||||
let response = srv.execute(request.send()).unwrap();
|
||||
assert!(response.status().is_success());
|
||||
|
||||
// read response
|
||||
let bytes = srv.execute(response.body()).unwrap();
|
||||
assert_eq!(bytes, Bytes::from_static(b"Welcome 10!"));
|
||||
|
||||
// client request
|
||||
let request = srv
|
||||
.get()
|
||||
.uri(srv.url("/sc/test1/index.html"))
|
||||
.finish()
|
||||
.unwrap();
|
||||
let response = srv.execute(request.send()).unwrap();
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_scope_and_path_extractor() {
|
||||
let mut srv = test::TestServer::with_factory(move || {
|
||||
App::new().scope("/sc", |scope| {
|
||||
scope.nested("/{num}", |scope| {
|
||||
scope.resource("/{num}/index.html", |r| {
|
||||
r.route().with(|p: Path<(usize, usize)>| {
|
||||
format!("Welcome {} {}!", p.0, p.1)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
// client request
|
||||
let request = srv
|
||||
.get()
|
||||
.uri(srv.url("/sc/10/12/index.html"))
|
||||
.finish()
|
||||
.unwrap();
|
||||
let response = srv.execute(request.send()).unwrap();
|
||||
assert!(response.status().is_success());
|
||||
|
||||
// read response
|
||||
let bytes = srv.execute(response.body()).unwrap();
|
||||
assert_eq!(bytes, Bytes::from_static(b"Welcome 10 12!"));
|
||||
|
||||
// client request
|
||||
let request = srv
|
||||
.get()
|
||||
.uri(srv.url("/sc/10/test1/index.html"))
|
||||
.finish()
|
||||
.unwrap();
|
||||
let response = srv.execute(request.send()).unwrap();
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[cfg(actix_impl_trait)]
|
||||
fn test_impl_trait(
|
||||
data: (Json<Value>, Path<PParam>, Query<PParam>),
|
||||
|
@@ -54,6 +54,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
|
||||
Hello World Hello World Hello World Hello World Hello World";
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_start() {
|
||||
let _ = test::TestServer::unused_addr();
|
||||
let (tx, rx) = mpsc::channel();
|
||||
@@ -86,12 +87,17 @@ fn test_start() {
|
||||
// pause
|
||||
let _ = srv_addr.send(server::PauseServer).wait();
|
||||
thread::sleep(time::Duration::from_millis(200));
|
||||
assert!(net::TcpStream::connect(addr).is_err());
|
||||
{
|
||||
let req = client::ClientRequest::get(format!("http://{}/", addr).as_str())
|
||||
.timeout(time::Duration::from_millis(200))
|
||||
.finish()
|
||||
.unwrap();
|
||||
assert!(sys.run_until_complete(req.send()).is_err());
|
||||
}
|
||||
|
||||
// resume
|
||||
let _ = srv_addr.send(server::ResumeServer).wait();
|
||||
thread::sleep(time::Duration::from_millis(200));
|
||||
|
||||
thread::sleep(time::Duration::from_millis(400));
|
||||
{
|
||||
let req = client::ClientRequest::get(format!("http://{}/", addr).as_str())
|
||||
.finish()
|
||||
|
Reference in New Issue
Block a user