Skip to content

Commit

Permalink
http_interop: Implement Request conversion for http::request::Parts
Browse files Browse the repository at this point in the history
I found the current conversion for `http::request::Builder` to be
rather useless when the `http` crate and various crates providing `http`
interfaces like `oauth2` are designed to provide an `http::Request`
directly, and there being no way to convert from a `http::Request`
back into its `http::request::Builder`.  That, together with strange
infallible defaults instead of providing  `TryFrom` make the current
implementation cumbersome to use.

Fortunately `http` provides `http::Request::into_parts()` to get
back a `Parts` structure (which is wrapped in `Result<>` inside
`http::request::Builder` as sole member!) together with the request body
(a generic type) which the user can manually pass to `send_string()`,
`send_bytes()` or `call()` if there's no data.

Implement a `From<http::request::Parts> for ureq::Request` to support
this case, making `ureq` finally capable of sending `http::Request`s.
(Note that, despite exclusively consisting of `Result<Parts>`,
 `http::request::Builder` has no constructor from `Ok(Parts {..})` which
 would have also facilitated this use-case somewhat)
  • Loading branch information
MarijnS95 committed Oct 10, 2023
1 parent 10e1c4a commit e0c532d
Showing 1 changed file with 84 additions and 31 deletions.
115 changes: 84 additions & 31 deletions src/http_interop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@ use std::{

use crate::{header::HeaderLine, response::ResponseStatusIndex, Request, Response};

/// Converts an [http::Response] into a [Response](crate::Response).
/// Converts an [`http::Response`] into a [`Response`].
///
/// As an [http::Response] does not contain a URL, `"https://example.com/"` is
/// used as a placeholder. Additionally, if the response has a header which
/// cannot be converted into a valid [Header](crate::Header), it will be skipped
/// rather than having the conversion fail. The remote address property will
/// also always be `127.0.0.1:80` for similar reasons to the URL.
/// As an [`http::Response`] does not contain a URL, `"https://example.com/"` is used as a
/// placeholder. Additionally, if the response has a header which cannot be converted into a valid
/// [`Header`](crate::Header), it will be skipped rather than having the conversion fail. The remote
/// address property will also always be `127.0.0.1:80` for similar reasons to the URL.
///
/// Requires feature `ureq = { version = "*", features = ["http"] }`
/// ```
Expand Down Expand Up @@ -81,12 +80,11 @@ fn create_builder(response: &Response) -> http::response::Builder {
response_builder
}

/// Converts a [Response](crate::Response) into an [http::Response], where the
/// body is a reader containing the body of the response.
/// Converts a [`Response`] into an [`http::Response`], where the body is a reader containing the
/// body of the response.
///
/// Due to slight differences in how headers are handled, this means if a header
/// from a [Response](crate::Response) is not valid UTF-8, it will not be
/// included in the resulting [http::Response].
/// Due to slight differences in how headers are handled, this means if a header from a [`Response`]
/// is not valid UTF-8, it will not be included in the resulting [`http::Response`].
///
/// Requires feature `ureq = { version = "*", features = ["http"] }`
/// ```
Expand All @@ -104,12 +102,10 @@ impl From<Response> for http::Response<Box<dyn Read + Send + Sync + 'static>> {
}
}

/// Converts a [Response](crate::Response) into an [http::Response], where the
/// body is a String.
/// Converts a [`Response`] into an [`http::Response`], where the body is a String.
///
/// Due to slight differences in how headers are handled, this means if a header
/// from a [Response](crate::Response) is not valid UTF-8, it will not be
/// included in the resulting [http::Response].
/// Due to slight differences in how headers are handled, this means if a header from a [`Response`]
/// is not valid UTF-8, it will not be included in the resulting [`http::Response`].
///
/// Requires feature `ureq = { version = "*", features = ["http"] }`
/// ```
Expand All @@ -128,12 +124,10 @@ impl From<Response> for http::Response<String> {
}
}

/// Converts a [Response](crate::Response) into an [http::Response], where the
/// body is a Vec<u8>.
/// Converts a [`Response`] into an [`http::Response`], where the body is a [`Vec<u8>`].
///
/// Due to slight differences in how headers are handled, this means if a header
/// from a [Response](crate::Response) is not valid UTF-8, it will not be
/// included in the resulting [http::Response].
/// Due to slight differences in how headers are handled, this means if a header from a [`Response`]
/// is not valid UTF-8, it will not be included in the resulting [`http::Response`].
///
/// Requires feature `ureq = { version = "*", features = ["http"] }`
/// ```
Expand All @@ -152,12 +146,12 @@ impl From<Response> for http::Response<Vec<u8>> {
}
}

/// Converts an [http] [Builder](http::request::Builder) into a [Request](crate::Request)
/// Converts an [`http::request::Builder`] into a [`Request`].
///
/// This will safely handle cases where a builder is not fully "complete" to
/// prevent the conversion from failing. Should the requests' method or URI not
/// be correctly set, the request will default to being a GET request to
/// `"https://example.com"`. Additionally, any non-UTF8 headers will be skipped.
/// This will safely handle cases where a builder is not fully "complete" to prevent the conversion
/// from failing. Should the requests' method or URI not be correctly set, the request will default
/// to being a `GET` request to `"https://example.com"`. Additionally, any non-UTF8 headers will
/// be skipped.
///
/// Requires feature `ureq = { version = "*", features = ["http"] }`
/// ```
Expand All @@ -169,6 +163,24 @@ impl From<Response> for http::Response<Vec<u8>> {
/// # Ok(())
/// # }
/// ```
///
/// # Converting from [`http::Request`]
///
/// Notably `ureq` does _not_ implement the conversion from [`http::Request`] because it contains
/// the body of a request together with the actual request data. However, [`http`] provides
/// [`http::Request::into_parts()`] to split out a request into [`http::request::Parts`] and a
/// `body`, for which the conversion _is_ implemented and can be used as follows:
///
/// ```
/// # fn main() -> Result<(), ureq::Error> {
/// # ureq::is_test(true);
/// let http_request = http::Request::builder().method("GET").uri("http://example.com").body(vec![0u8]).unwrap();
/// let (http_parts, body) = http_request.into_parts();
/// let request: ureq::Request = http_parts.into();
/// request.send_bytes(&body)?;
/// # Ok(())
/// # }
/// ```
impl From<http::request::Builder> for Request {
fn from(value: http::request::Builder) -> Self {
let mut new_request = crate::agent().request(
Expand Down Expand Up @@ -197,11 +209,52 @@ impl From<http::request::Builder> for Request {
}
}

/// Converts a [Request](crate::Request) into an [http] [Builder](http::request::Builder).
/// Converts [`http::request::Parts`] into a [`Request`].
///
/// Due to slight differences in how headers are handled, this means if a header from
/// [`http::request::Parts`] is not valid UTF-8, it will not be included in the resulting
/// [`Response`].
///
/// Requires feature `ureq = { version = "*", features = ["http"] }`
///
/// An [`http::Request`] can be split out into its [`http::request::Parts`] and body as follows:
///
/// ```
/// # fn main() -> Result<(), ureq::Error> {
/// # ureq::is_test(true);
/// let http_request = http::Request::builder().method("GET").uri("http://example.com").body(vec![0u8]).unwrap();
/// let (http_parts, body) = http_request.into_parts();
/// let request: ureq::Request = http_parts.into();
/// request.send_bytes(&body)?;
/// # Ok(())
/// # }
/// ```
impl From<http::request::Parts> for Request {
fn from(value: http::request::Parts) -> Self {
let mut new_request = crate::agent().request(value.method.as_str(), &value.uri.to_string());

new_request = value
.headers
.iter()
.filter_map(|header| {
header
.1
.to_str()
.ok()
.map(|str_value| (header.0.as_str(), str_value))
})
.fold(new_request, |request, header| {
request.set(header.0, header.1)
});

new_request
}
}

/// Converts a [`Request`] into an [`http::request::Builder`].
///
/// This will only convert valid UTF-8 header values into headers on the
/// resulting builder. The method and URI are preserved. The HTTP version will
/// always be set to `HTTP/1.1`.
/// This will only convert valid UTF-8 header values into headers on the resulting builder. The
/// method and URI are preserved. The HTTP version will always be set to `HTTP/1.1`.
///
/// Requires feature `ureq = { version = "*", features = ["http"] }`
/// ```
Expand Down Expand Up @@ -240,7 +293,7 @@ mod tests {
fn convert_http_response() {
use http::{Response, StatusCode, Version};

let http_response_body = (0..10240).into_iter().map(|_| 0xaa).collect::<Vec<u8>>();
let http_response_body = vec![0xaa; 10240];
let http_response = Response::builder()
.version(Version::HTTP_2)
.header("Custom-Header", "custom value")
Expand Down

0 comments on commit e0c532d

Please sign in to comment.