diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 3f5e25330..8d6ea759a 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -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(¶ms) -//! .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(¶ms) +//! .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> { -//! 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(()) //! # } //! ``` diff --git a/awc/src/middleware/redirect.rs b/awc/src/middleware/redirect.rs index 198e32a35..d48822168 100644 --- a/awc/src/middleware/redirect.rs +++ b/awc/src/middleware/redirect.rs @@ -246,26 +246,32 @@ where } fn build_next_uri(res: &ClientResponse, prev_uri: &Uri) -> Result { - 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) } @@ -294,7 +300,7 @@ mod tests { }; #[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)) @@ -319,6 +325,27 @@ 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()