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

prepare awc release 3.0.0 (#2684)

This commit is contained in:
Rob Ede
2022-03-08 16:51:40 +00:00
committed by GitHub
parent 87f627cd5d
commit 8ddb24b49b
19 changed files with 306 additions and 93 deletions

View File

@ -246,7 +246,12 @@ where
///
/// The default limit size is 100.
pub fn limit(mut self, limit: usize) -> Self {
self.config.limit = limit;
if limit == 0 {
self.config.limit = u32::MAX as usize;
} else {
self.config.limit = limit;
}
self
}

View File

@ -83,12 +83,12 @@ where
false
};
framed.send((head, body.size()).into()).await?;
let mut pin_framed = Pin::new(&mut framed);
// special handle for EXPECT request.
let (do_send, mut res_head) = if is_expect {
pin_framed.send((head, body.size()).into()).await?;
let head = poll_fn(|cx| pin_framed.as_mut().poll_next(cx))
.await
.ok_or(ConnectError::Disconnected)??;
@ -97,13 +97,17 @@ where
// and current head would be used as final response head.
(head.status == StatusCode::CONTINUE, Some(head))
} else {
pin_framed.feed((head, body.size()).into()).await?;
(true, None)
};
if do_send {
// send request body
match body.size() {
BodySize::None | BodySize::Sized(0) => {}
BodySize::None | BodySize::Sized(0) => {
poll_fn(|cx| pin_framed.as_mut().flush(cx)).await?;
}
_ => send_body(body, pin_framed.as_mut()).await?,
};

View File

@ -30,17 +30,35 @@ pub type BoxConnectorService = Rc<
pub type BoxedSocket = Box<dyn ConnectionIo>;
/// Combined HTTP and WebSocket request type received by connection service.
pub enum ConnectRequest {
/// Standard HTTP request.
///
/// Contains the request head, body type, and optional pre-resolved socket address.
Client(RequestHeadType, AnyBody, Option<net::SocketAddr>),
/// Tunnel used by WebSocket connection requests.
///
/// Contains the request head and optional pre-resolved socket address.
Tunnel(RequestHead, Option<net::SocketAddr>),
}
/// Combined HTTP response & WebSocket tunnel type returned from connection service.
pub enum ConnectResponse {
/// Standard HTTP response.
Client(ClientResponse),
/// Tunnel used for WebSocket communication.
///
/// Contains response head and framed HTTP/1.1 codec.
Tunnel(ResponseHead, Framed<BoxedSocket, ClientCodec>),
}
impl ConnectResponse {
/// Unwraps type into HTTP response.
///
/// # Panics
/// Panics if enum variant is not `Client`.
pub fn into_client_response(self) -> ClientResponse {
match self {
ConnectResponse::Client(res) => res,
@ -50,6 +68,10 @@ impl ConnectResponse {
}
}
/// Unwraps type into WebSocket tunnel response.
///
/// # Panics
/// Panics if enum variant is not `Tunnel`.
pub fn into_tunnel_response(self) -> (ResponseHead, Framed<BoxedSocket, ClientCodec>) {
match self {
ConnectResponse::Tunnel(head, framed) => (head, framed),
@ -136,30 +158,37 @@ where
ConnectRequestProj::Connection { fut, req } => {
let connection = ready!(fut.poll(cx))?;
let req = req.take().unwrap();
match req {
ConnectRequest::Client(head, body, ..) => {
// send request
let fut = ConnectRequestFuture::Client {
fut: connection.send_request(head, body),
};
self.set(fut);
}
ConnectRequest::Tunnel(head, ..) => {
// send request
let fut = ConnectRequestFuture::Tunnel {
fut: connection.open_tunnel(RequestHeadType::from(head)),
};
self.set(fut);
}
}
self.poll(cx)
}
ConnectRequestProj::Client { fut } => {
let (head, payload) = ready!(fut.as_mut().poll(cx))?;
Poll::Ready(Ok(ConnectResponse::Client(ClientResponse::new(
head, payload,
))))
}
ConnectRequestProj::Tunnel { fut } => {
let (head, framed) = ready!(fut.as_mut().poll(cx))?;
let framed = framed.into_map_io(|io| Box::new(io) as _);

View File

@ -1,22 +1,25 @@
//! `awc` is an asynchronous HTTP and WebSocket client library.
//!
//! # Making a GET request
//! # `GET` Requests
//! ```no_run
//! # #[actix_rt::main]
//! # async fn main() -> Result<(), awc::error::SendRequestError> {
//! // create client
//! let mut client = awc::Client::default();
//! let response = client.get("http://www.rust-lang.org") // <- Create request builder
//! .insert_header(("User-Agent", "Actix-web"))
//! .send() // <- Send http request
//! .await?;
//!
//! println!("Response: {:?}", response);
//! // construct request
//! let req = client.get("http://www.rust-lang.org")
//! .insert_header(("User-Agent", "awc/3.0"));
//!
//! // send request and await response
//! let res = req.send().await?;
//! println!("Response: {:?}", res);
//! # Ok(())
//! # }
//! ```
//!
//! # Making POST requests
//! ## Raw body contents
//! # `POST` Requests
//! ## Raw Body
//! ```no_run
//! # #[actix_rt::main]
//! # async fn main() -> Result<(), awc::error::SendRequestError> {
@ -28,20 +31,6 @@
//! # }
//! ```
//!
//! ## Forms
//! ```no_run
//! # #[actix_rt::main]
//! # async fn main() -> Result<(), awc::error::SendRequestError> {
//! let params = [("foo", "bar"), ("baz", "quux")];
//!
//! let mut client = awc::Client::default();
//! let response = client.post("http://httpbin.org/post")
//! .send_form(&params)
//! .await?;
//! # Ok(())
//! # }
//! ```
//!
//! ## JSON
//! ```no_run
//! # #[actix_rt::main]
@ -59,6 +48,20 @@
//! # }
//! ```
//!
//! ## URL Encoded Form
//! ```no_run
//! # #[actix_rt::main]
//! # async fn main() -> Result<(), awc::error::SendRequestError> {
//! let params = [("foo", "bar"), ("baz", "quux")];
//!
//! let mut client = awc::Client::default();
//! let response = client.post("http://httpbin.org/post")
//! .send_form(&params)
//! .await?;
//! # Ok(())
//! # }
//! ```
//!
//! # Response Compression
//! All [official][iana-encodings] and common content encoding codecs are supported, optionally.
//!
@ -76,11 +79,12 @@
//!
//! [iana-encodings]: https://www.iana.org/assignments/http-parameters/http-parameters.xhtml#content-coding
//!
//! # WebSocket support
//! # WebSockets
//! ```no_run
//! # #[actix_rt::main]
//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! use futures_util::{sink::SinkExt, stream::StreamExt};
//! use futures_util::{sink::SinkExt as _, stream::StreamExt as _};
//!
//! let (_resp, mut connection) = awc::Client::new()
//! .ws("ws://echo.websocket.org")
//! .connect()
@ -89,8 +93,9 @@
//! connection
//! .send(awc::ws::Message::Text("Echo".into()))
//! .await?;
//!
//! let response = connection.next().await.unwrap()?;
//! # assert_eq!(response, awc::ws::Frame::Text("Echo".as_bytes().into()));
//! assert_eq!(response, awc::ws::Frame::Text("Echo".into()));
//! # Ok(())
//! # }
//! ```

View File

@ -161,7 +161,8 @@ where
| StatusCode::SEE_OTHER
| StatusCode::TEMPORARY_REDIRECT
| StatusCode::PERMANENT_REDIRECT
if *max_redirect_times > 0 =>
if *max_redirect_times > 0
&& res.headers().contains_key(header::LOCATION) =>
{
let reuse_body = res.head().status == StatusCode::TEMPORARY_REDIRECT
|| res.head().status == StatusCode::PERMANENT_REDIRECT;
@ -245,26 +246,32 @@ where
}
fn build_next_uri(res: &ClientResponse, prev_uri: &Uri) -> Result<Uri, SendRequestError> {
let uri = res
.headers()
.get(header::LOCATION)
.map(|value| {
// try to parse the location to a full uri
let uri = Uri::try_from(value.as_bytes())
.map_err(|e| SendRequestError::Url(InvalidUrl::HttpError(e.into())))?;
if uri.scheme().is_none() || uri.authority().is_none() {
let uri = Uri::builder()
.scheme(prev_uri.scheme().cloned().unwrap())
.authority(prev_uri.authority().cloned().unwrap())
.path_and_query(value.as_bytes())
.build()?;
Ok::<_, SendRequestError>(uri)
} else {
Ok(uri)
}
})
// TODO: this error type is wrong.
.ok_or(SendRequestError::Url(InvalidUrl::MissingScheme))??;
// responses without this header are not processed
let location = res.headers().get(header::LOCATION).unwrap();
// try to parse the location and resolve to a full URI but fall back to default if it fails
let uri = Uri::try_from(location.as_bytes()).unwrap_or_else(|_| Uri::default());
let uri = if uri.scheme().is_none() || uri.authority().is_none() {
let builder = Uri::builder()
.scheme(prev_uri.scheme().cloned().unwrap())
.authority(prev_uri.authority().cloned().unwrap());
// when scheme or authority is missing treat the location value as path and query
// recover error where location does not have leading slash
let path = if location.as_bytes().starts_with(b"/") {
location.as_bytes().to_owned()
} else {
[b"/", location.as_bytes()].concat()
};
builder
.path_and_query(path)
.build()
.map_err(|err| SendRequestError::Url(InvalidUrl::HttpError(err)))?
} else {
uri
};
Ok(uri)
}
@ -287,10 +294,13 @@ mod tests {
use actix_web::{web, App, Error, HttpRequest, HttpResponse};
use super::*;
use crate::{http::header::HeaderValue, ClientBuilder};
use crate::{
http::{header::HeaderValue, StatusCode},
ClientBuilder,
};
#[actix_rt::test]
async fn test_basic_redirect() {
async fn basic_redirect() {
let client = ClientBuilder::new()
.disable_redirects()
.wrap(Redirect::new().max_redirect_times(10))
@ -315,6 +325,44 @@ mod tests {
assert_eq!(res.status().as_u16(), 400);
}
#[actix_rt::test]
async fn redirect_relative_without_leading_slash() {
let client = ClientBuilder::new().finish();
let srv = actix_test::start(|| {
App::new()
.service(web::resource("/").route(web::to(|| async {
HttpResponse::Found()
.insert_header(("location", "abc/"))
.finish()
})))
.service(
web::resource("/abc/")
.route(web::to(|| async { HttpResponse::Accepted().finish() })),
)
});
let res = client.get(srv.url("/")).send().await.unwrap();
assert_eq!(res.status(), StatusCode::ACCEPTED);
}
#[actix_rt::test]
async fn redirect_without_location() {
let client = ClientBuilder::new()
.disable_redirects()
.wrap(Redirect::new().max_redirect_times(10))
.finish();
let srv = actix_test::start(|| {
App::new().service(web::resource("/").route(web::to(|| async {
Ok::<_, Error>(HttpResponse::Found().finish())
})))
});
let res = client.get(srv.url("/")).send().await.unwrap();
assert_eq!(res.status(), StatusCode::FOUND);
}
#[actix_rt::test]
async fn test_redirect_limit() {
let client = ClientBuilder::new()
@ -328,14 +376,14 @@ mod tests {
.service(web::resource("/").route(web::to(|| async {
Ok::<_, Error>(
HttpResponse::Found()
.append_header(("location", "/test"))
.insert_header(("location", "/test"))
.finish(),
)
})))
.service(web::resource("/test").route(web::to(|| async {
Ok::<_, Error>(
HttpResponse::Found()
.append_header(("location", "/test2"))
.insert_header(("location", "/test2"))
.finish(),
)
})))
@ -345,8 +393,15 @@ mod tests {
});
let res = client.get(srv.url("/")).send().await.unwrap();
assert_eq!(res.status().as_u16(), 302);
assert_eq!(res.status(), StatusCode::FOUND);
assert_eq!(
res.headers()
.get(header::LOCATION)
.unwrap()
.to_str()
.unwrap(),
"/test2"
);
}
#[actix_rt::test]

View File

@ -505,7 +505,7 @@ impl fmt::Debug for ClientRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
"\nClientRequest {:?} {}:{}",
"\nClientRequest {:?} {} {}",
self.head.version, self.head.method, self.head.uri
)?;
writeln!(f, " headers:")?;