1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-08-27 07:25:54 +02:00

Compare commits

..

25 Commits

Author SHA1 Message Date
Nikolay Kim
cc6e0c6d04 Fix client payload decompression #674 2019-03-28 20:40:25 -07:00
Zeyi Fan
d9496d46d1 [0.7] Fix never-ending HTTP2 empty response (#737)
* Fix never-ending HTTP2 empty response #737
2019-03-28 17:40:12 -07:00
Jannik Keye
bf8262196f feat: enable use of patch as request method (#718) 2019-03-14 11:36:10 +03:00
Luca Bruno
17ecdd63d2 httpresponse: add constructor for HttpResponseBuilder (#697) 2019-03-13 17:20:18 +03:00
David McGuire
cc7f6b5eef Fix preflight CORS header compliance; refactor previous patch. (#717) 2019-03-11 07:26:54 +03:00
Stephen Ellis
ceca96da28 Added HTTP Authentication for Client (#540) 2019-03-06 12:56:12 +03:00
Douman
42f030d3f4 Ensure that Content-Length zero is specified in empty request 2019-03-05 08:37:15 +03:00
Hugo Benício
6d11ee683f fixing little typo in docs (#711) 2019-03-01 11:34:58 +03:00
Douman
80d4cbe301 Add change notes for new HttpResponseBuilder 2019-02-27 21:37:20 +03:00
Kornel
69d710dbce Add insert and remove() to response builder (#707) 2019-02-27 15:52:42 +03:00
Michael Edwards
0059a55dfb Fix typo 2019-02-13 14:31:28 +03:00
cuebyte
c695358bcb Ignored the If-Modified-Since if If-None-Match is specified (#680) (#692) 2019-02-09 00:33:00 +03:00
Jason Hills
b018e4abaf Fixes TestRequest::with_cookie panic 2019-02-07 07:55:27 +03:00
Vladislav Stepanov
346d85a884 Serve static file directly instead of redirecting (#676) 2019-02-04 13:20:46 +03:00
wildarch
9968afe4a6 Use NamedFile with an existing File (#670) 2019-01-28 08:07:28 +03:00
Tomas Izquierdo Garcia-Faria
f5bec968c7 Bump v_htmlescape version to 0.4 2019-01-25 11:31:42 +03:00
Neil Jensen
a534fdd125 Add io handling for ECONNRESET when data has already been received 2019-01-20 08:45:33 +03:00
rishflab
3431fff4d7 Fixed example in client documentation. This closes #665. 2019-01-14 07:44:30 +03:00
Sameer Puri
d6df2e3399 Fix HttpResponse doc spelling "os" to "of" 2019-01-11 08:45:15 +03:00
Douman
1fbb52ad3b 0.7.18 Bump 2019-01-10 17:05:18 +03:00
Julian Tescher
e5cdd22720 Fix test server listener thread leak (#655) 2019-01-08 10:42:22 -08:00
Douman
4f2e970732 Tidy up CHANGES.md 2019-01-08 10:49:03 +03:00
Douman
4d45313f9d Decode special characters when handling static files 2019-01-08 10:46:58 +03:00
Juan Aguilar
55a2a59906 Improve change askama_escape in favor of v_htmlescape (#651) 2019-01-03 22:34:18 +03:00
Ji Qu
61883042c2 Add with-cookie init-method for TestRequest (#647) 2019-01-02 13:24:08 +03:00
19 changed files with 650 additions and 141 deletions

View File

@@ -1,5 +1,44 @@
# Changes # Changes
## [0.7.19] - 2019-03-29
### Added
* Add `from_file` and `from_file_with_config` to `NamedFile` to allow sending files without a known path. #670
* Add `insert` and `remove` methods to `HttpResponseBuilder`
* Add client HTTP Authentication methods `.basic_auth()` and `.bearer_auth()`. #540
* Add support for PATCH HTTP method
### Fixed
* Ignored the `If-Modified-Since` if `If-None-Match` is specified. #680
* Do not remove `Content-Length` on `Body::Empty` and insert zero value if it is missing for `POST` and `PUT` methods.
* Fix preflight CORS header compliance; refactor previous patch (#603). #717
* Fix never-ending HTTP2 request when response is empty (#709). #737
* Fix client payload decompression #674
## [0.7.18] - 2019-01-10
### Added
* Add `with_cookie` for `TestRequest` to allow users to customize request cookie. #647
* Add `cookie` method for `TestRequest` to allow users to add cookie dynamically.
### Fixed
* StaticFiles decode special characters in request's path
* Fix test server listener leak #654
## [0.7.17] - 2018-12-25 ## [0.7.17] - 2018-12-25
### Added ### Added

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "actix-web" name = "actix-web"
version = "0.7.17" version = "0.7.19"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust."
readme = "README.md" readme = "README.md"
@@ -64,7 +64,7 @@ cell = ["actix-net/cell"]
actix = "0.7.9" actix = "0.7.9"
actix-net = "0.2.6" actix-net = "0.2.6"
askama_escape = "0.1.0" v_htmlescape = "0.4"
base64 = "0.10" base64 = "0.10"
bitflags = "1.0" bitflags = "1.0"
failure = "^0.1.2" failure = "^0.1.2"

View File

@@ -5,9 +5,9 @@
//! # extern crate actix; //! # extern crate actix;
//! # extern crate futures; //! # extern crate futures;
//! # extern crate tokio; //! # extern crate tokio;
//! # use futures::Future;
//! # use std::process; //! # use std::process;
//! use actix_web::client; //! use actix_web::client;
//! use futures::Future;
//! //!
//! fn main() { //! fn main() {
//! actix::run( //! actix::run(
@@ -66,9 +66,9 @@ impl ResponseError for SendRequestError {
/// # extern crate futures; /// # extern crate futures;
/// # extern crate tokio; /// # extern crate tokio;
/// # extern crate env_logger; /// # extern crate env_logger;
/// # use futures::Future;
/// # use std::process; /// # use std::process;
/// use actix_web::client; /// use actix_web::client;
/// use futures::Future;
/// ///
/// fn main() { /// fn main() {
/// actix::run( /// actix::run(
@@ -105,6 +105,13 @@ pub fn post<U: AsRef<str>>(uri: U) -> ClientRequestBuilder {
builder builder
} }
/// Create request builder for `PATCH` requests
pub fn patch<U: AsRef<str>>(uri: U) -> ClientRequestBuilder {
let mut builder = ClientRequest::build();
builder.method(Method::PATCH).uri(uri);
builder
}
/// Create request builder for `PUT` requests /// Create request builder for `PUT` requests
pub fn put<U: AsRef<str>>(uri: U) -> ClientRequestBuilder { pub fn put<U: AsRef<str>>(uri: U) -> ClientRequestBuilder {
let mut builder = ClientRequest::build(); let mut builder = ClientRequest::build();

View File

@@ -6,8 +6,8 @@ use std::time::{Duration, Instant};
use std::{io, mem}; use std::{io, mem};
use tokio_timer::Delay; use tokio_timer::Delay;
use actix_inner::dev::Request;
use actix::{Addr, SystemService}; use actix::{Addr, SystemService};
use actix_inner::dev::Request;
use super::{ use super::{
ClientConnector, ClientConnectorError, ClientRequest, ClientResponse, Connect, ClientConnector, ClientConnectorError, ClientRequest, ClientResponse, Connect,
@@ -88,7 +88,8 @@ impl SendRequest {
} }
pub(crate) fn with_connector( pub(crate) fn with_connector(
req: ClientRequest, conn: Addr<ClientConnector>, req: ClientRequest,
conn: Addr<ClientConnector>,
) -> SendRequest { ) -> SendRequest {
SendRequest { SendRequest {
req, req,
@@ -363,11 +364,11 @@ impl Pipeline {
if let Some(ref mut decompress) = self.decompress { if let Some(ref mut decompress) = self.decompress {
match decompress.feed_data(b) { match decompress.feed_data(b) {
Ok(Some(b)) => return Ok(Async::Ready(Some(b))), Ok(Some(b)) => return Ok(Async::Ready(Some(b))),
Ok(None) => return Ok(Async::NotReady), Ok(None) => continue,
Err(ref err) Err(ref err)
if err.kind() == io::ErrorKind::WouldBlock => if err.kind() == io::ErrorKind::WouldBlock =>
{ {
continue continue;
} }
Err(err) => return Err(err.into()), Err(err) => return Err(err.into()),
} }

View File

@@ -12,6 +12,7 @@ use serde::Serialize;
use serde_json; use serde_json;
use serde_urlencoded; use serde_urlencoded;
use url::Url; use url::Url;
use base64::encode;
use super::connector::{ClientConnector, Connection}; use super::connector::{ClientConnector, Connection};
use super::pipeline::SendRequest; use super::pipeline::SendRequest;
@@ -111,6 +112,13 @@ impl ClientRequest {
builder builder
} }
/// Create request builder for `PATCH` request
pub fn patch<U: AsRef<str>>(uri: U) -> ClientRequestBuilder {
let mut builder = ClientRequest::build();
builder.method(Method::PATCH).uri(uri);
builder
}
/// Create request builder for `PUT` request /// Create request builder for `PUT` request
pub fn put<U: AsRef<str>>(uri: U) -> ClientRequestBuilder { pub fn put<U: AsRef<str>>(uri: U) -> ClientRequestBuilder {
let mut builder = ClientRequest::build(); let mut builder = ClientRequest::build();
@@ -485,6 +493,29 @@ impl ClientRequestBuilder {
self self
} }
/// Set HTTP basic authorization
pub fn basic_auth<U, P>(&mut self, username: U, password: Option<P>) -> &mut Self
where
U: fmt::Display,
P: fmt::Display,
{
let auth = match password {
Some(password) => format!("{}:{}", username, password),
None => format!("{}", username)
};
let header_value = format!("Basic {}", encode(&auth));
self.header(header::AUTHORIZATION, &*header_value)
}
/// Set HTTP bearer authentication
pub fn bearer_auth<T>( &mut self, token: T) -> &mut Self
where
T: fmt::Display,
{
let header_value = format!("Bearer {}", token);
self.header(header::AUTHORIZATION, &*header_value)
}
/// Set content length /// Set content length
#[inline] #[inline]
pub fn content_length(&mut self, len: u64) -> &mut Self { pub fn content_length(&mut self, len: u64) -> &mut Self {

View File

@@ -16,9 +16,9 @@ use flate2::write::{GzEncoder, ZlibEncoder};
use flate2::Compression; use flate2::Compression;
use futures::{Async, Poll}; use futures::{Async, Poll};
use http::header::{ use http::header::{
HeaderValue, CONNECTION, CONTENT_ENCODING, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, self, HeaderValue, CONNECTION, CONTENT_ENCODING, CONTENT_LENGTH, DATE, TRANSFER_ENCODING,
}; };
use http::{HttpTryFrom, Version}; use http::{Method, HttpTryFrom, Version};
use time::{self, Duration}; use time::{self, Duration};
use tokio_io::AsyncWrite; use tokio_io::AsyncWrite;
@@ -223,7 +223,19 @@ fn content_encoder(buf: BytesMut, req: &mut ClientRequest) -> Output {
let transfer = match body { let transfer = match body {
Body::Empty => { Body::Empty => {
req.headers_mut().remove(CONTENT_LENGTH); match req.method() {
//Insert zero content-length only if user hasn't added it.
//We don't really need it for other methods as they are not supposed to carry payload
&Method::POST | &Method::PUT | &Method::PATCH => {
req.headers_mut()
.entry(CONTENT_LENGTH)
.expect("CONTENT_LENGTH to be valid header name")
.or_insert(header::HeaderValue::from_static("0"));
},
_ => {
req.headers_mut().remove(CONTENT_LENGTH);
}
}
return Output::Empty(buf); return Output::Empty(buf);
} }
Body::Binary(ref mut bytes) => { Body::Binary(ref mut bytes) => {
@@ -410,3 +422,76 @@ impl CachedDate {
self.next_update.nsec = 0; self.next_update.nsec = 0;
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_content_encoder_empty_body() {
let mut req = ClientRequest::post("http://google.com").finish().expect("Create request");
let result = content_encoder(BytesMut::new(), &mut req);
match result {
Output::Empty(buf) => {
assert_eq!(buf.len(), 0);
let content_len = req.headers().get(CONTENT_LENGTH).expect("To set Content-Length for empty POST");
assert_eq!(content_len, "0");
},
_ => panic!("Unexpected result, should be Output::Empty"),
}
req.set_method(Method::GET);
let result = content_encoder(BytesMut::new(), &mut req);
match result {
Output::Empty(buf) => {
assert_eq!(buf.len(), 0);
assert!(!req.headers().contains_key(CONTENT_LENGTH));
},
_ => panic!("Unexpected result, should be Output::Empty"),
}
req.set_method(Method::PUT);
let result = content_encoder(BytesMut::new(), &mut req);
match result {
Output::Empty(buf) => {
assert_eq!(buf.len(), 0);
let content_len = req.headers().get(CONTENT_LENGTH).expect("To set Content-Length for empty PUT");
assert_eq!(content_len, "0");
},
_ => panic!("Unexpected result, should be Output::Empty"),
}
req.set_method(Method::DELETE);
let result = content_encoder(BytesMut::new(), &mut req);
match result {
Output::Empty(buf) => {
assert_eq!(buf.len(), 0);
assert!(!req.headers().contains_key(CONTENT_LENGTH));
},
_ => panic!("Unexpected result, should be Output::Empty"),
}
req.set_method(Method::PATCH);
let result = content_encoder(BytesMut::new(), &mut req);
match result {
Output::Empty(buf) => {
assert_eq!(buf.len(), 0);
let content_len = req.headers().get(CONTENT_LENGTH).expect("To set Content-Length for empty PATCH");
assert_eq!(content_len, "0");
},
_ => panic!("Unexpected result, should be Output::Empty"),
}
}
}

View File

@@ -193,7 +193,7 @@ impl<T: fmt::Display> fmt::Display for Path<T> {
} }
#[derive(PartialEq, Eq, PartialOrd, Ord)] #[derive(PartialEq, Eq, PartialOrd, Ord)]
/// Extract typed information from from the request's query. /// Extract typed information from the request's query.
/// ///
/// ## Example /// ## Example
/// ///

255
src/fs.rs
View File

@@ -11,7 +11,7 @@ use std::{cmp, io};
#[cfg(unix)] #[cfg(unix)]
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
use askama_escape::{escape as escape_html_entity}; use v_htmlescape::escape as escape_html_entity;
use bytes::Bytes; use bytes::Bytes;
use futures::{Async, Future, Poll, Stream}; use futures::{Async, Future, Poll, Stream};
use futures_cpupool::{CpuFuture, CpuPool}; use futures_cpupool::{CpuFuture, CpuPool};
@@ -120,6 +120,32 @@ pub struct NamedFile<C = DefaultConfig> {
} }
impl NamedFile { impl NamedFile {
/// Creates an instance from a previously opened file.
///
/// The given `path` need not exist and is only used to determine the `ContentType` and
/// `ContentDisposition` headers.
///
/// # Examples
///
/// ```no_run
/// extern crate actix_web;
///
/// use actix_web::fs::NamedFile;
/// use std::io::{self, Write};
/// use std::env;
/// use std::fs::File;
///
/// fn main() -> io::Result<()> {
/// let mut file = File::create("foo.txt")?;
/// file.write_all(b"Hello, world!")?;
/// let named_file = NamedFile::from_file(file, "bar.txt")?;
/// Ok(())
/// }
/// ```
pub fn from_file<P: AsRef<Path>>(file: File, path: P) -> io::Result<NamedFile> {
Self::from_file_with_config(file, path, DefaultConfig)
}
/// Attempts to open a file in read-only mode. /// Attempts to open a file in read-only mode.
/// ///
/// # Examples /// # Examples
@@ -135,16 +161,29 @@ impl NamedFile {
} }
impl<C: StaticFileConfig> NamedFile<C> { impl<C: StaticFileConfig> NamedFile<C> {
/// Attempts to open a file in read-only mode using provided configiration. /// Creates an instance from a previously opened file using the provided configuration.
///
/// The given `path` need not exist and is only used to determine the `ContentType` and
/// `ContentDisposition` headers.
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust /// ```no_run
/// use actix_web::fs::{DefaultConfig, NamedFile}; /// extern crate actix_web;
/// ///
/// let file = NamedFile::open_with_config("foo.txt", DefaultConfig); /// use actix_web::fs::{DefaultConfig, NamedFile};
/// use std::io::{self, Write};
/// use std::env;
/// use std::fs::File;
///
/// fn main() -> io::Result<()> {
/// let mut file = File::create("foo.txt")?;
/// file.write_all(b"Hello, world!")?;
/// let named_file = NamedFile::from_file_with_config(file, "bar.txt", DefaultConfig)?;
/// Ok(())
/// }
/// ``` /// ```
pub fn open_with_config<P: AsRef<Path>>(path: P, _: C) -> io::Result<NamedFile<C>> { pub fn from_file_with_config<P: AsRef<Path>>(file: File, path: P, _: C) -> io::Result<NamedFile<C>> {
let path = path.as_ref().to_path_buf(); let path = path.as_ref().to_path_buf();
// Get the name of the file and use it to construct default Content-Type // Get the name of the file and use it to construct default Content-Type
@@ -169,7 +208,6 @@ impl<C: StaticFileConfig> NamedFile<C> {
(ct, cd) (ct, cd)
}; };
let file = File::open(&path)?;
let md = file.metadata()?; let md = file.metadata()?;
let modified = md.modified().ok(); let modified = md.modified().ok();
let cpu_pool = None; let cpu_pool = None;
@@ -188,6 +226,19 @@ impl<C: StaticFileConfig> NamedFile<C> {
}) })
} }
/// Attempts to open a file in read-only mode using provided configuration.
///
/// # Examples
///
/// ```rust
/// use actix_web::fs::{DefaultConfig, NamedFile};
///
/// let file = NamedFile::open_with_config("foo.txt", DefaultConfig);
/// ```
pub fn open_with_config<P: AsRef<Path>>(path: P, config: C) -> io::Result<NamedFile<C>> {
Self::from_file_with_config(File::open(&path)?, path, config)
}
/// Returns reference to the underlying `File` object. /// Returns reference to the underlying `File` object.
#[inline] #[inline]
pub fn file(&self) -> &File { pub fn file(&self) -> &File {
@@ -390,6 +441,8 @@ impl<C: StaticFileConfig> Responder for NamedFile<C> {
// check last modified // check last modified
let not_modified = if !none_match(etag.as_ref(), req) { let not_modified = if !none_match(etag.as_ref(), req) {
true true
} else if req.headers().contains_key(header::IF_NONE_MATCH) {
false
} else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) = } else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) =
(last_modified, req.get_header()) (last_modified, req.get_header())
{ {
@@ -739,7 +792,7 @@ impl<S: 'static, C: StaticFileConfig> StaticFiles<S, C> {
/// Set index file /// Set index file
/// ///
/// Redirects to specific index file for directory "/" instead of /// Shows specific index file for directory "/" instead of
/// showing files listing. /// showing files listing.
pub fn index_file<T: Into<String>>(mut self, index: T) -> StaticFiles<S, C> { pub fn index_file<T: Into<String>>(mut self, index: T) -> StaticFiles<S, C> {
self.index = Some(index.into()); self.index = Some(index.into());
@@ -756,7 +809,7 @@ impl<S: 'static, C: StaticFileConfig> StaticFiles<S, C> {
&self, &self,
req: &HttpRequest<S>, req: &HttpRequest<S>,
) -> Result<AsyncResult<HttpResponse>, Error> { ) -> Result<AsyncResult<HttpResponse>, Error> {
let tail: String = req.match_info().query("tail")?; let tail: String = req.match_info().get_decoded("tail").unwrap_or_else(|| "".to_string());
let relpath = PathBuf::from_param(tail.trim_left_matches('/'))?; let relpath = PathBuf::from_param(tail.trim_left_matches('/'))?;
// full filepath // full filepath
@@ -764,17 +817,11 @@ impl<S: 'static, C: StaticFileConfig> StaticFiles<S, C> {
if path.is_dir() { if path.is_dir() {
if let Some(ref redir_index) = self.index { if let Some(ref redir_index) = self.index {
// TODO: Don't redirect, just return the index content. let path = path.join(redir_index);
// TODO: It'd be nice if there were a good usable URL manipulation
// library NamedFile::open_with_config(path, C::default())?
let mut new_path: String = req.path().to_owned(); .set_cpu_pool(self.cpu_pool.clone())
if !new_path.ends_with('/') { .respond_to(&req)?
new_path.push('/');
}
new_path.push_str(redir_index);
HttpResponse::Found()
.header(header::LOCATION, new_path.as_str())
.finish()
.respond_to(&req) .respond_to(&req)
} else if self.show_index { } else if self.show_index {
let dir = Directory::new(self.directory.clone(), path); let dir = Directory::new(self.directory.clone(), path);
@@ -899,6 +946,8 @@ impl HttpRange {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::fs; use std::fs;
use std::time::Duration;
use std::ops::Add;
use super::*; use super::*;
use application::App; use application::App;
@@ -918,6 +967,43 @@ mod tests {
assert_eq!(m, mime::APPLICATION_OCTET_STREAM); assert_eq!(m, mime::APPLICATION_OCTET_STREAM);
} }
#[test]
fn test_if_modified_since_without_if_none_match() {
let mut file = NamedFile::open("Cargo.toml")
.unwrap()
.set_cpu_pool(CpuPool::new(1));
let since = header::HttpDate::from(
SystemTime::now().add(Duration::from_secs(60)));
let req = TestRequest::default()
.header(header::IF_MODIFIED_SINCE, since)
.finish();
let resp = file.respond_to(&req).unwrap();
assert_eq!(
resp.status(),
StatusCode::NOT_MODIFIED
);
}
#[test]
fn test_if_modified_since_with_if_none_match() {
let mut file = NamedFile::open("Cargo.toml")
.unwrap()
.set_cpu_pool(CpuPool::new(1));
let since = header::HttpDate::from(
SystemTime::now().add(Duration::from_secs(60)));
let req = TestRequest::default()
.header(header::IF_NONE_MATCH, "miss_etag")
.header(header::IF_MODIFIED_SINCE, since)
.finish();
let resp = file.respond_to(&req).unwrap();
assert_ne!(
resp.status(),
StatusCode::NOT_MODIFIED
);
}
#[test] #[test]
fn test_named_file_text() { fn test_named_file_text() {
assert!(NamedFile::open("test--").is_err()); assert!(NamedFile::open("test--").is_err());
@@ -1298,6 +1384,27 @@ mod tests {
assert_eq!(bytes, data); assert_eq!(bytes, data);
} }
#[test]
fn test_static_files_with_spaces() {
let mut srv = test::TestServer::with_factory(|| {
App::new().handler(
"/",
StaticFiles::new(".").unwrap().index_file("Cargo.toml"),
)
});
let request = srv
.get()
.uri(srv.url("/tests/test%20space.binary"))
.finish()
.unwrap();
let response = srv.execute(request.send()).unwrap();
assert_eq!(response.status(), StatusCode::OK);
let bytes = srv.execute(response.body()).unwrap();
let data = Bytes::from(fs::read("tests/test space.binary").unwrap());
assert_eq!(bytes, data);
}
#[derive(Default)] #[derive(Default)]
pub struct OnlyMethodHeadConfig; pub struct OnlyMethodHeadConfig;
impl StaticFileConfig for OnlyMethodHeadConfig { impl StaticFileConfig for OnlyMethodHeadConfig {
@@ -1410,43 +1517,66 @@ mod tests {
} }
#[test] #[test]
fn test_redirect_to_index() { fn test_serve_index() {
let st = StaticFiles::new(".").unwrap().index_file("index.html"); let st = StaticFiles::new(".").unwrap().index_file("test.binary");
let req = TestRequest::default().uri("/tests").finish(); let req = TestRequest::default().uri("/tests").finish();
let resp = st.handle(&req).respond_to(&req).unwrap(); let resp = st.handle(&req).respond_to(&req).unwrap();
let resp = resp.as_msg(); let resp = resp.as_msg();
assert_eq!(resp.status(), StatusCode::FOUND); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!( assert_eq!(
resp.headers().get(header::LOCATION).unwrap(), resp.headers().get(header::CONTENT_TYPE).expect("content type"),
"/tests/index.html" "application/octet-stream"
);
assert_eq!(
resp.headers().get(header::CONTENT_DISPOSITION).expect("content disposition"),
"attachment; filename=\"test.binary\""
); );
let req = TestRequest::default().uri("/tests/").finish(); let req = TestRequest::default().uri("/tests/").finish();
let resp = st.handle(&req).respond_to(&req).unwrap(); let resp = st.handle(&req).respond_to(&req).unwrap();
let resp = resp.as_msg(); let resp = resp.as_msg();
assert_eq!(resp.status(), StatusCode::FOUND); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!( assert_eq!(
resp.headers().get(header::LOCATION).unwrap(), resp.headers().get(header::CONTENT_TYPE).unwrap(),
"/tests/index.html" "application/octet-stream"
); );
assert_eq!(
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
"attachment; filename=\"test.binary\""
);
// nonexistent index file
let req = TestRequest::default().uri("/tests/unknown").finish();
let resp = st.handle(&req).respond_to(&req).unwrap();
let resp = resp.as_msg();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let req = TestRequest::default().uri("/tests/unknown/").finish();
let resp = st.handle(&req).respond_to(&req).unwrap();
let resp = resp.as_msg();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
} }
#[test] #[test]
fn test_redirect_to_index_nested() { fn test_serve_index_nested() {
let st = StaticFiles::new(".").unwrap().index_file("mod.rs"); let st = StaticFiles::new(".").unwrap().index_file("mod.rs");
let req = TestRequest::default().uri("/src/client").finish(); let req = TestRequest::default().uri("/src/client").finish();
let resp = st.handle(&req).respond_to(&req).unwrap(); let resp = st.handle(&req).respond_to(&req).unwrap();
let resp = resp.as_msg(); let resp = resp.as_msg();
assert_eq!(resp.status(), StatusCode::FOUND); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!( assert_eq!(
resp.headers().get(header::LOCATION).unwrap(), resp.headers().get(header::CONTENT_TYPE).unwrap(),
"/src/client/mod.rs" "text/x-rust"
);
assert_eq!(
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
"inline; filename=\"mod.rs\""
); );
} }
#[test] #[test]
fn integration_redirect_to_index_with_prefix() { fn integration_serve_index_with_prefix() {
let mut srv = test::TestServer::with_factory(|| { let mut srv = test::TestServer::with_factory(|| {
App::new() App::new()
.prefix("public") .prefix("public")
@@ -1455,29 +1585,21 @@ mod tests {
let request = srv.get().uri(srv.url("/public")).finish().unwrap(); let request = srv.get().uri(srv.url("/public")).finish().unwrap();
let response = srv.execute(request.send()).unwrap(); let response = srv.execute(request.send()).unwrap();
assert_eq!(response.status(), StatusCode::FOUND); assert_eq!(response.status(), StatusCode::OK);
let loc = response let bytes = srv.execute(response.body()).unwrap();
.headers() let data = Bytes::from(fs::read("Cargo.toml").unwrap());
.get(header::LOCATION) assert_eq!(bytes, data);
.unwrap()
.to_str()
.unwrap();
assert_eq!(loc, "/public/Cargo.toml");
let request = srv.get().uri(srv.url("/public/")).finish().unwrap(); let request = srv.get().uri(srv.url("/public/")).finish().unwrap();
let response = srv.execute(request.send()).unwrap(); let response = srv.execute(request.send()).unwrap();
assert_eq!(response.status(), StatusCode::FOUND); assert_eq!(response.status(), StatusCode::OK);
let loc = response let bytes = srv.execute(response.body()).unwrap();
.headers() let data = Bytes::from(fs::read("Cargo.toml").unwrap());
.get(header::LOCATION) assert_eq!(bytes, data);
.unwrap()
.to_str()
.unwrap();
assert_eq!(loc, "/public/Cargo.toml");
} }
#[test] #[test]
fn integration_redirect_to_index() { fn integration_serve_index() {
let mut srv = test::TestServer::with_factory(|| { let mut srv = test::TestServer::with_factory(|| {
App::new().handler( App::new().handler(
"test", "test",
@@ -1487,25 +1609,26 @@ mod tests {
let request = srv.get().uri(srv.url("/test")).finish().unwrap(); let request = srv.get().uri(srv.url("/test")).finish().unwrap();
let response = srv.execute(request.send()).unwrap(); let response = srv.execute(request.send()).unwrap();
assert_eq!(response.status(), StatusCode::FOUND); assert_eq!(response.status(), StatusCode::OK);
let loc = response let bytes = srv.execute(response.body()).unwrap();
.headers() let data = Bytes::from(fs::read("Cargo.toml").unwrap());
.get(header::LOCATION) assert_eq!(bytes, data);
.unwrap()
.to_str()
.unwrap();
assert_eq!(loc, "/test/Cargo.toml");
let request = srv.get().uri(srv.url("/test/")).finish().unwrap(); let request = srv.get().uri(srv.url("/test/")).finish().unwrap();
let response = srv.execute(request.send()).unwrap(); let response = srv.execute(request.send()).unwrap();
assert_eq!(response.status(), StatusCode::FOUND); assert_eq!(response.status(), StatusCode::OK);
let loc = response let bytes = srv.execute(response.body()).unwrap();
.headers() let data = Bytes::from(fs::read("Cargo.toml").unwrap());
.get(header::LOCATION) assert_eq!(bytes, data);
.unwrap()
.to_str() // nonexistent index file
.unwrap(); let request = srv.get().uri(srv.url("/test/unknown")).finish().unwrap();
assert_eq!(loc, "/test/Cargo.toml"); let response = srv.execute(request.send()).unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
let request = srv.get().uri(srv.url("/test/unknown/")).finish().unwrap();
let response = srv.execute(request.send()).unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
} }
#[test] #[test]

View File

@@ -48,10 +48,10 @@ impl HttpResponse {
self.0.as_mut() self.0.as_mut()
} }
/// Create http response builder with specific status. /// Create a new HTTP response builder with specific status.
#[inline] #[inline]
pub fn build(status: StatusCode) -> HttpResponseBuilder { pub fn build(status: StatusCode) -> HttpResponseBuilder {
HttpResponsePool::get(status) HttpResponseBuilder::new(status)
} }
/// Create http response builder /// Create http response builder
@@ -246,7 +246,7 @@ impl HttpResponse {
self self
} }
/// Get body os this response /// Get body of this response
#[inline] #[inline]
pub fn body(&self) -> &Body { pub fn body(&self) -> &Body {
&self.get_ref().body &self.get_ref().body
@@ -346,6 +346,12 @@ pub struct HttpResponseBuilder {
} }
impl HttpResponseBuilder { impl HttpResponseBuilder {
/// Create a new HTTP response builder with specific status.
#[inline]
pub fn new(status: StatusCode) -> HttpResponseBuilder {
HttpResponsePool::get(status)
}
/// Set HTTP status code of this response. /// Set HTTP status code of this response.
#[inline] #[inline]
pub fn status(&mut self, status: StatusCode) -> &mut Self { pub fn status(&mut self, status: StatusCode) -> &mut Self {
@@ -366,7 +372,7 @@ impl HttpResponseBuilder {
self self
} }
/// Set a header. /// Append a header.
/// ///
/// ```rust /// ```rust
/// # extern crate actix_web; /// # extern crate actix_web;
@@ -394,7 +400,7 @@ impl HttpResponseBuilder {
self self
} }
/// Set a header. /// Append a header.
/// ///
/// ```rust /// ```rust
/// # extern crate actix_web; /// # extern crate actix_web;
@@ -426,6 +432,65 @@ impl HttpResponseBuilder {
} }
self self
} }
/// Set or replace a header with a single value.
///
/// ```rust
/// # extern crate actix_web;
/// use actix_web::{http, HttpRequest, HttpResponse};
///
/// fn index(req: HttpRequest) -> HttpResponse {
/// HttpResponse::Ok()
/// .insert("X-TEST", "value")
/// .insert(http::header::CONTENT_TYPE, "application/json")
/// .finish()
/// }
/// fn main() {}
/// ```
pub fn insert<K, V>(&mut self, key: K, value: V) -> &mut Self
where
HeaderName: HttpTryFrom<K>,
V: IntoHeaderValue,
{
if let Some(parts) = parts(&mut self.response, &self.err) {
match HeaderName::try_from(key) {
Ok(key) => match value.try_into() {
Ok(value) => {
parts.headers.insert(key, value);
}
Err(e) => self.err = Some(e.into()),
},
Err(e) => self.err = Some(e.into()),
};
}
self
}
/// Remove all instances of a header already set on this `HttpResponseBuilder`.
///
/// ```rust
/// # extern crate actix_web;
/// use actix_web::{http, HttpRequest, HttpResponse};
///
/// fn index(req: HttpRequest) -> HttpResponse {
/// HttpResponse::Ok()
/// .header(http::header::CONTENT_TYPE, "nevermind") // won't be used
/// .remove(http::header::CONTENT_TYPE)
/// .finish()
/// }
/// ```
pub fn remove<K>(&mut self, key: K) -> &mut Self
where HeaderName: HttpTryFrom<K>
{
if let Some(parts) = parts(&mut self.response, &self.err) {
match HeaderName::try_from(key) {
Ok(key) => {
parts.headers.remove(key);
},
Err(e) => self.err = Some(e.into()),
};
}
self
}
/// Set the custom reason for the response. /// Set the custom reason for the response.
#[inline] #[inline]
@@ -1118,6 +1183,14 @@ mod tests {
assert_eq!((v.name(), v.value()), ("cookie3", "val300")); assert_eq!((v.name(), v.value()), ("cookie3", "val300"));
} }
#[test]
fn test_builder_new() {
let resp = HttpResponseBuilder::new(StatusCode::BAD_REQUEST)
.finish();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[test] #[test]
fn test_basic_builder() { fn test_basic_builder() {
let resp = HttpResponse::Ok() let resp = HttpResponse::Ok()
@@ -1128,6 +1201,40 @@ mod tests {
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
} }
#[test]
fn test_insert() {
let resp = HttpResponse::Ok()
.insert("deleteme", "old value")
.insert("deleteme", "new value")
.finish();
assert_eq!("new value", resp.headers().get("deleteme").expect("new value"));
}
#[test]
fn test_remove() {
let resp = HttpResponse::Ok()
.header("deleteme", "value")
.remove("deleteme")
.finish();
assert!(resp.headers().get("deleteme").is_none())
}
#[test]
fn test_remove_replace() {
let resp = HttpResponse::Ok()
.header("some-header", "old_value1")
.header("some-header", "old_value2")
.remove("some-header")
.header("some-header", "new_value1")
.header("some-header", "new_value2")
.remove("unrelated-header")
.finish();
let mut v = resp.headers().get_all("some-header").into_iter();
assert_eq!("new_value1", v.next().unwrap());
assert_eq!("new_value2", v.next().unwrap());
assert_eq!(None, v.next());
}
#[test] #[test]
fn test_upgrade() { fn test_upgrade() {
let resp = HttpResponse::build(StatusCode::OK).upgrade().finish(); let resp = HttpResponse::build(StatusCode::OK).upgrade().finish();

View File

@@ -100,7 +100,6 @@ extern crate failure;
extern crate lazy_static; extern crate lazy_static;
#[macro_use] #[macro_use]
extern crate futures; extern crate futures;
extern crate askama_escape;
extern crate cookie; extern crate cookie;
extern crate futures_cpupool; extern crate futures_cpupool;
extern crate http as modhttp; extern crate http as modhttp;
@@ -137,6 +136,7 @@ extern crate serde_urlencoded;
extern crate percent_encoding; extern crate percent_encoding;
extern crate serde_json; extern crate serde_json;
extern crate smallvec; extern crate smallvec;
extern crate v_htmlescape;
extern crate actix_net; extern crate actix_net;
#[macro_use] #[macro_use]

View File

@@ -307,6 +307,32 @@ impl Cors {
} }
} }
fn access_control_allow_origin(&self, req: &Request) -> Option<HeaderValue> {
match self.inner.origins {
AllOrSome::All => {
if self.inner.send_wildcard {
Some(HeaderValue::from_static("*"))
} else if let Some(origin) = req.headers().get(header::ORIGIN) {
Some(origin.clone())
} else {
None
}
}
AllOrSome::Some(ref origins) => {
if let Some(origin) = req.headers().get(header::ORIGIN).filter(|o| {
match o.to_str() {
Ok(os) => origins.contains(os),
_ => false
}
}) {
Some(origin.clone())
} else {
Some(self.inner.origins_str.as_ref().unwrap().clone())
}
}
}
}
fn validate_allowed_method(&self, req: &Request) -> Result<(), CorsError> { fn validate_allowed_method(&self, req: &Request) -> Result<(), CorsError> {
if let Some(hdr) = req.headers().get(header::ACCESS_CONTROL_REQUEST_METHOD) { if let Some(hdr) = req.headers().get(header::ACCESS_CONTROL_REQUEST_METHOD) {
if let Ok(meth) = hdr.to_str() { if let Ok(meth) = hdr.to_str() {
@@ -390,21 +416,9 @@ impl<S> Middleware<S> for Cors {
}).if_some(headers, |headers, resp| { }).if_some(headers, |headers, resp| {
let _ = let _ =
resp.header(header::ACCESS_CONTROL_ALLOW_HEADERS, headers); resp.header(header::ACCESS_CONTROL_ALLOW_HEADERS, headers);
}).if_true(self.inner.origins.is_all(), |resp| { }).if_some(self.access_control_allow_origin(&req), |origin, resp| {
if self.inner.send_wildcard { let _ =
resp.header(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*"); resp.header(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin);
} else {
let origin = req.headers().get(header::ORIGIN).unwrap();
resp.header(
header::ACCESS_CONTROL_ALLOW_ORIGIN,
origin.clone(),
);
}
}).if_true(self.inner.origins.is_some(), |resp| {
resp.header(
header::ACCESS_CONTROL_ALLOW_ORIGIN,
self.inner.origins_str.as_ref().unwrap().clone(),
);
}).if_true(self.inner.supports_credentials, |resp| { }).if_true(self.inner.supports_credentials, |resp| {
resp.header(header::ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); resp.header(header::ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
}).header( }).header(
@@ -430,37 +444,11 @@ impl<S> Middleware<S> for Cors {
fn response( fn response(
&self, req: &HttpRequest<S>, mut resp: HttpResponse, &self, req: &HttpRequest<S>, mut resp: HttpResponse,
) -> Result<Response> { ) -> Result<Response> {
match self.inner.origins {
AllOrSome::All => { if let Some(origin) = self.access_control_allow_origin(req) {
if self.inner.send_wildcard { resp.headers_mut()
resp.headers_mut().insert( .insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin.clone());
header::ACCESS_CONTROL_ALLOW_ORIGIN, };
HeaderValue::from_static("*"),
);
} else if let Some(origin) = req.headers().get(header::ORIGIN) {
resp.headers_mut()
.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin.clone());
}
}
AllOrSome::Some(ref origins) => {
if let Some(origin) = req.headers().get(header::ORIGIN).filter(|o| {
match o.to_str() {
Ok(os) => origins.contains(os),
_ => false
}
}) {
resp.headers_mut().insert(
header::ACCESS_CONTROL_ALLOW_ORIGIN,
origin.clone(),
);
} else {
resp.headers_mut().insert(
header::ACCESS_CONTROL_ALLOW_ORIGIN,
self.inner.origins_str.as_ref().unwrap().clone()
);
};
}
}
if let Some(ref expose) = self.inner.expose_hdrs { if let Some(ref expose) = self.inner.expose_hdrs {
resp.headers_mut().insert( resp.headers_mut().insert(
@@ -1201,7 +1189,6 @@ mod tests {
let resp: HttpResponse = HttpResponse::Ok().into(); let resp: HttpResponse = HttpResponse::Ok().into();
let resp = cors.response(&req, resp).unwrap().response(); let resp = cors.response(&req, resp).unwrap().response();
print!("{:?}", resp);
assert_eq!( assert_eq!(
&b"https://example.com"[..], &b"https://example.com"[..],
resp.headers() resp.headers()
@@ -1224,4 +1211,42 @@ mod tests {
.as_bytes() .as_bytes()
); );
} }
#[test]
fn test_multiple_origins_preflight() {
let cors = Cors::build()
.allowed_origin("https://example.com")
.allowed_origin("https://example.org")
.allowed_methods(vec![Method::GET])
.finish();
let req = TestRequest::with_header("Origin", "https://example.com")
.header(header::ACCESS_CONTROL_REQUEST_METHOD, "GET")
.method(Method::OPTIONS)
.finish();
let resp = cors.start(&req).ok().unwrap().response();
assert_eq!(
&b"https://example.com"[..],
resp.headers()
.get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
.unwrap()
.as_bytes()
);
let req = TestRequest::with_header("Origin", "https://example.org")
.header(header::ACCESS_CONTROL_REQUEST_METHOD, "GET")
.method(Method::OPTIONS)
.finish();
let resp = cors.start(&req).ok().unwrap().response();
assert_eq!(
&b"https://example.org"[..],
resp.headers()
.get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
.unwrap()
.as_bytes()
);
}
} }

View File

@@ -107,6 +107,12 @@ impl<S: 'static> Resource<S> {
self.routes.last_mut().unwrap().filter(pred::Post()) self.routes.last_mut().unwrap().filter(pred::Post())
} }
/// Register a new `PATCH` route.
pub fn patch(&mut self) -> &mut Route<S> {
self.routes.push(Route::default());
self.routes.last_mut().unwrap().filter(pred::Patch())
}
/// Register a new `PUT` route. /// Register a new `PUT` route.
pub fn put(&mut self) -> &mut Route<S> { pub fn put(&mut self) -> &mut Route<S> {
self.routes.push(Route::default()); self.routes.push(Route::default());

View File

@@ -234,6 +234,16 @@ impl<H: 'static> Writer for H2Writer<H> {
stream.reserve_capacity(cmp::min(self.buffer.len(), CHUNK_SIZE)); stream.reserve_capacity(cmp::min(self.buffer.len(), CHUNK_SIZE));
} }
if self.flags.contains(Flags::EOF)
&& !self.flags.contains(Flags::RESERVED)
&& self.buffer.is_empty()
{
if let Err(e) = stream.send_data(Bytes::new(), true) {
return Err(io::Error::new(io::ErrorKind::Other, e));
}
return Ok(Async::Ready(()));
}
loop { loop {
match stream.poll_capacity() { match stream.poll_capacity() {
Ok(Async::NotReady) => return Ok(Async::NotReady), Ok(Async::NotReady) => return Ok(Async::NotReady),

View File

@@ -451,9 +451,8 @@ impl<H: IntoHttpHandler, F: Fn() -> H + Send + Clone> HttpServer<H, F> {
/// For each address this method starts separate thread which does /// For each address this method starts separate thread which does
/// `accept()` in a loop. /// `accept()` in a loop.
/// ///
/// This methods panics if no socket addresses get bound. /// This methods panics if no socket address can be bound or an `Actix` system is not yet
/// /// configured.
/// This method requires to run within properly configured `Actix` system.
/// ///
/// ```rust /// ```rust
/// extern crate actix_web; /// extern crate actix_web;

View File

@@ -303,6 +303,8 @@ pub trait IoStream: AsyncRead + AsyncWrite + 'static {
} else { } else {
Ok(Async::NotReady) Ok(Async::NotReady)
} }
} else if e.kind() == io::ErrorKind::ConnectionReset && read_some {
Ok(Async::Ready((read_some, true)))
} else { } else {
Err(e) Err(e)
}; };

View File

@@ -5,7 +5,9 @@ use std::sync::mpsc;
use std::{net, thread}; use std::{net, thread};
use actix::{Actor, Addr, System}; use actix::{Actor, Addr, System};
use actix::actors::signal;
use actix_net::server::Server;
use cookie::Cookie; use cookie::Cookie;
use futures::Future; use futures::Future;
use http::header::HeaderName; use http::header::HeaderName;
@@ -66,6 +68,7 @@ pub struct TestServer {
ssl: bool, ssl: bool,
conn: Addr<ClientConnector>, conn: Addr<ClientConnector>,
rt: Runtime, rt: Runtime,
backend: Addr<Server>,
} }
impl TestServer { impl TestServer {
@@ -112,24 +115,25 @@ impl TestServer {
let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap(); let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap();
let local_addr = tcp.local_addr().unwrap(); let local_addr = tcp.local_addr().unwrap();
let _ = HttpServer::new(factory) let srv = HttpServer::new(factory)
.disable_signals() .disable_signals()
.listen(tcp) .listen(tcp)
.keep_alive(5) .keep_alive(5)
.start(); .start();
tx.send((System::current(), local_addr, TestServer::get_conn())) tx.send((System::current(), local_addr, TestServer::get_conn(), srv))
.unwrap(); .unwrap();
sys.run(); sys.run();
}); });
let (system, addr, conn) = rx.recv().unwrap(); let (system, addr, conn, backend) = rx.recv().unwrap();
System::set_current(system); System::set_current(system);
TestServer { TestServer {
addr, addr,
conn, conn,
ssl: false, ssl: false,
rt: Runtime::new().unwrap(), rt: Runtime::new().unwrap(),
backend,
} }
} }
@@ -197,6 +201,7 @@ impl TestServer {
/// Stop http server /// Stop http server
fn stop(&mut self) { fn stop(&mut self) {
let _ = self.backend.send(signal::Signal(signal::SignalType::Term)).wait();
System::current().stop(); System::current().stop();
} }
@@ -234,6 +239,11 @@ impl TestServer {
ClientRequest::post(self.url("/").as_str()) ClientRequest::post(self.url("/").as_str())
} }
/// Create `PATCH` request
pub fn patch(&self) -> ClientRequestBuilder {
ClientRequest::patch(self.url("/").as_str())
}
/// Create `HEAD` request /// Create `HEAD` request
pub fn head(&self) -> ClientRequestBuilder { pub fn head(&self) -> ClientRequestBuilder {
ClientRequest::head(self.url("/").as_str()) ClientRequest::head(self.url("/").as_str())
@@ -333,8 +343,7 @@ where
.keep_alive(5) .keep_alive(5)
.disable_signals(); .disable_signals();
tx.send((System::current(), addr, TestServer::get_conn()))
.unwrap();
#[cfg(any(feature = "alpn", feature = "ssl"))] #[cfg(any(feature = "alpn", feature = "ssl"))]
{ {
@@ -356,18 +365,22 @@ where
let tcp = net::TcpListener::bind(addr).unwrap(); let tcp = net::TcpListener::bind(addr).unwrap();
srv = srv.listen(tcp); srv = srv.listen(tcp);
} }
srv.start(); let backend = srv.start();
tx.send((System::current(), addr, TestServer::get_conn(), backend))
.unwrap();
sys.run(); sys.run();
}); });
let (system, addr, conn) = rx.recv().unwrap(); let (system, addr, conn, backend) = rx.recv().unwrap();
System::set_current(system); System::set_current(system);
TestServer { TestServer {
addr, addr,
conn, conn,
ssl: has_ssl, ssl: has_ssl,
rt: Runtime::new().unwrap(), rt: Runtime::new().unwrap(),
backend,
} }
} }
} }
@@ -507,6 +520,11 @@ impl TestRequest<()> {
{ {
TestRequest::default().header(key, value) TestRequest::default().header(key, value)
} }
/// Create TestRequest and set request cookie
pub fn with_cookie(cookie: Cookie<'static>) -> TestRequest<()> {
TestRequest::default().cookie(cookie)
}
} }
impl<S: 'static> TestRequest<S> { impl<S: 'static> TestRequest<S> {
@@ -543,6 +561,25 @@ impl<S: 'static> TestRequest<S> {
self self
} }
/// set cookie of this request
pub fn cookie(mut self, cookie: Cookie<'static>) -> Self {
if self.cookies.is_some() {
let mut should_insert = true;
let old_cookies = self.cookies.as_mut().unwrap();
for old_cookie in old_cookies.iter() {
if old_cookie == &cookie {
should_insert = false
};
};
if should_insert {
old_cookies.push(cookie);
};
} else {
self.cookies = Some(vec![cookie]);
};
self
}
/// Set a header /// Set a header
pub fn set<H: Header>(mut self, hdr: H) -> Self { pub fn set<H: Header>(mut self, hdr: H) -> Self {
if let Ok(value) = hdr.try_into() { if let Ok(value) = hdr.try_into() {

1
tests/test space.binary Normal file
View File

@@ -0,0 +1 @@
<EFBFBD>TǑɂV<EFBFBD>2<EFBFBD>vI<EFBFBD><EFBFBD><EFBFBD>\<5C><52><CB99><EFBFBD>e<EFBFBD><04>vD<76>:藽<>RV<03>Yp<59><70>;<3B><>G<><47>p!2<7F>C<EFBFBD>.<2E> <0C><><EFBFBD><EFBFBD>pA !<21>ߦ<EFBFBD>x j+Uc<55><63><EFBFBD>X<13>c%<17>;<3B>"y<10><>AI

View File

@@ -506,3 +506,31 @@ fn client_read_until_eof() {
let bytes = sys.block_on(response.body()).unwrap(); let bytes = sys.block_on(response.body()).unwrap();
assert_eq!(bytes, Bytes::from_static(b"welcome!")); assert_eq!(bytes, Bytes::from_static(b"welcome!"));
} }
#[test]
fn client_basic_auth() {
let mut srv =
test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok().body(STR)));
/// set authorization header to Basic <base64 encoded username:password>
let request = srv
.get()
.basic_auth("username", Some("password"))
.finish()
.unwrap();
let repr = format!("{:?}", request);
assert!(repr.contains("Basic dXNlcm5hbWU6cGFzc3dvcmQ="));
}
#[test]
fn client_bearer_auth() {
let mut srv =
test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok().body(STR)));
/// set authorization header to Bearer <token>
let request = srv
.get()
.bearer_auth("someS3cr3tAutht0k3n")
.finish()
.unwrap();
let repr = format!("{:?}", request);
assert!(repr.contains("Bearer someS3cr3tAutht0k3n"));
}

View File

@@ -1398,3 +1398,11 @@ fn test_content_length() {
assert_eq!(response.headers().get(&header), Some(&value)); assert_eq!(response.headers().get(&header), Some(&value));
} }
} }
#[test]
fn test_patch_method() {
let mut srv = test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok()));
let req = srv.patch().finish().unwrap();
let response = srv.execute(req.send()).unwrap();
assert!(response.status().is_success());
}