Repository: ruma/ruma
Pull request #168 (2020-07-29 21:18:13)

Use a dedicated method for conversion from Ruma request type to http::Request
The opposite conversion should probably also receive a dedicated conversion function that returns an `Option<String>` access token on success and fails if an access token is required but missing.

jimmycuadra: Can you provide some detail on what you have in mind in the issue description?
farodin91: For example: https://github.com/ruma/ruma/blob/master/src/api/r0/profile.rs#L340 and https://github.com/ruma/ruma/blob/master/src/api/r0/profile.rs#L361 are nearly the same test. We could write one macro to represent both and then generate these tests
jplatte: This makes almost all tests in matrix-rust-sdk fail, and it's not obvious why :(

I will try to make sense of the errors as soon as I can get to it.

Patch:
diff --git a/ruma-api-macros/src/api.rs b/ruma-api-macros/src/api.rs
index db87d774..dba3140f 100644
--- a/ruma-api-macros/src/api.rs
+++ b/ruma-api-macros/src/api.rs
@@ -93,7 +93,7 @@ impl ToTokens for Api {
 
         let non_auth_endpoint_impl = if requires_authentication.value {
             quote! {
-                impl ruma_api::NonAuthEndpoint for Request {}
+                impl ::ruma_api::NonAuthEndpoint for Request {}
             }
         } else {
             TokenStream::new()
@@ -116,7 +116,8 @@ impl ToTokens for Api {
 
         let extract_request_path = if self.request.has_path_fields() {
             quote! {
-                let path_segments: Vec<&str> = request.uri().path()[1..].split('/').collect();
+                let path_segments: ::std::vec::Vec<&::std::primitive::str> =
+                    request.uri().path()[1..].split('/').collect();
             }
         } else {
             TokenStream::new()
@@ -139,15 +140,20 @@ impl ToTokens for Api {
             self.request.request_init_query_fields()
         };
 
-        let add_headers_to_request = if self.request.has_header_fields() {
-            let add_headers = self.request.add_headers_to_request();
-            quote! {
-                let headers = http_request.headers_mut();
-                #add_headers
-            }
-        } else {
-            TokenStream::new()
-        };
+        let mut header_kvs = self.request.append_header_kvs();
+        if requires_authentication.value {
+            header_kvs.push(quote! {
+                ::ruma_api::exports::http::header::AUTHORIZATION,
+                ::ruma_api::exports::http::header::HeaderValue::from_str(
+                    &::std::format!(
+                        "Bearer {}",
+                        access_token.ok_or_else(
+                            ::ruma_api::error::IntoHttpError::needs_authentication
+                        )?
+                    )
+                )?
+            });
+        }
 
         let extract_request_headers = if self.request.has_header_fields() {
             quote! {
@@ -157,18 +163,18 @@ impl ToTokens for Api {
             TokenStream::new()
         };
 
-        let extract_request_body = if self.request.has_body_fields()
-            || self.request.newtype_body_field().is_some()
-        {
-            quote! {
-                let request_body: <RequestBody as ::ruma_api::Outgoing>::Incoming = ::ruma_api::try_deserialize!(
-                    request,
-                    ::ruma_api::exports::serde_json::from_slice(request.body().as_slice())
-                );
-            }
-        } else {
-            TokenStream::new()
-        };
+        let extract_request_body =
+            if self.request.has_body_fields() || self.request.newtype_body_field().is_some() {
+                quote! {
+                    let request_body: <RequestBody as ::ruma_api::Outgoing>::Incoming =
+                        ::ruma_api::try_deserialize!(
+                            request,
+                            ::ruma_api::exports::serde_json::from_slice(request.body().as_slice())
+                        );
+                }
+            } else {
+                TokenStream::new()
+            };
 
         let parse_request_headers = if self.request.has_header_fields() {
             self.request.parse_headers_from_request()
@@ -188,18 +194,18 @@ impl ToTokens for Api {
             TokenStream::new()
         };
 
-        let typed_response_body_decl = if self.response.has_body_fields()
-            || self.response.newtype_body_field().is_some()
-        {
-            quote! {
-                let response_body: <ResponseBody as ::ruma_api::Outgoing>::Incoming = ::ruma_api::try_deserialize!(
-                    response,
-                    ::ruma_api::exports::serde_json::from_slice(response.body().as_slice()),
-                );
-            }
-        } else {
-            TokenStream::new()
-        };
+        let typed_response_body_decl =
+            if self.response.has_body_fields() || self.response.newtype_body_field().is_some() {
+                quote! {
+                    let response_body: <ResponseBody as ::ruma_api::Outgoing>::Incoming =
+                        ::ruma_api::try_deserialize!(
+                            response,
+                            ::ruma_api::exports::serde_json::from_slice(response.body().as_slice()),
+                        );
+                }
+            } else {
+                TokenStream::new()
+            };
 
         let response_init_fields = self.response.init_fields();
 
@@ -217,20 +223,26 @@ impl ToTokens for Api {
         let error = &self.error;
 
         let api = quote! {
+            // FIXME: These can't conflict with other imports, but it would still be nice not to
+            //        bring anything into scope that code outside the macro could then rely on.
+            use ::std::convert::TryInto as _;
+
             use ::ruma_api::exports::serde::de::Error as _;
             use ::ruma_api::exports::serde::Deserialize as _;
             use ::ruma_api::Endpoint as _;
 
-            use std::convert::TryInto as _;
-
             #[doc = #request_doc]
             #request_type
 
-            impl std::convert::TryFrom<::ruma_api::exports::http::Request<Vec<u8>>> for #request_try_from_type {
+            impl ::std::convert::TryFrom<::ruma_api::exports::http::Request<Vec<u8>>>
+                for #request_try_from_type
+            {
                 type Error = ::ruma_api::error::FromHttpRequestError;
 
                 #[allow(unused_variables)]
-                fn try_from(request: ::ruma_api::exports::http::Request<Vec<u8>>) -> Result<Self, Self::Error> {
+                fn try_from(
+                    request: ::ruma_api::exports::http::Request<Vec<u8>>
+                ) -> ::std::result::Result<Self, Self::Error> {
                     #extract_request_path
                     #extract_request_query
                     #extract_request_headers
@@ -245,37 +257,16 @@ impl ToTokens for Api {
                 }
             }
 
-            impl std::convert::TryFrom<Request> for ::ruma_api::exports::http::Request<Vec<u8>> {
-                type Error = ::ruma_api::error::IntoHttpError;
-
-                #[allow(unused_mut, unused_variables)]
-                fn try_from(request: Request) -> Result<Self, Self::Error> {
-                    let metadata = Request::METADATA;
-                    let path_and_query = #request_path_string + &#request_query_string;
-                    let mut http_request = ::ruma_api::exports::http::Request::new(#request_body);
-
-                    *http_request.method_mut() = ::ruma_api::exports::http::Method::#method;
-                    *http_request.uri_mut() = ::ruma_api::exports::http::uri::Builder::new()
-                        .path_and_query(path_and_query.as_str())
-                        .build()
-                        // The ruma_api! macro guards against invalid path input, but if there are
-                        // invalid (non ASCII) bytes in the fields with the query attribute this will panic.
-                        .unwrap();
-
-                    { #add_headers_to_request }
-
-                    Ok(http_request)
-                }
-            }
-
             #[doc = #response_doc]
             #response_type
 
-            impl std::convert::TryFrom<Response> for ::ruma_api::exports::http::Response<Vec<u8>> {
+            impl ::std::convert::TryFrom<Response>
+                for ::ruma_api::exports::http::Response<Vec<u8>>
+            {
                 type Error = ::ruma_api::error::IntoHttpError;
 
                 #[allow(unused_variables)]
-                fn try_from(response: Response) -> Result<Self, Self::Error> {
+                fn try_from(response: Response) -> ::std::result::Result<Self, Self::Error> {
                     let response = ::ruma_api::exports::http::Response::builder()
                         .header(::ruma_api::exports::http::header::CONTENT_TYPE, "application/json")
                         #serialize_response_headers
@@ -287,13 +278,15 @@ impl ToTokens for Api {
                 }
             }
 
-            impl std::convert::TryFrom<::ruma_api::exports::http::Response<Vec<u8>>> for #response_try_from_type {
+            impl ::std::convert::TryFrom<::ruma_api::exports::http::Response<Vec<u8>>>
+                for #response_try_from_type
+            {
                 type Error = ::ruma_api::error::FromHttpResponseError<#error>;
 
                 #[allow(unused_variables)]
                 fn try_from(
                     response: ::ruma_api::exports::http::Response<Vec<u8>>,
-                ) -> Result<Self, Self::Error> {
+                ) -> ::std::result::Result<Self, Self::Error> {
                     if response.status().as_u16() < 400 {
                         #extract_response_headers
 
@@ -324,6 +317,31 @@ impl ToTokens for Api {
                     rate_limited: #rate_limited,
                     requires_authentication: #requires_authentication,
                 };
+
+                #[allow(unused_mut, unused_variables)]
+                fn try_into_http_request(
+                    self,
+                    base_url: &::std::primitive::str,
+                    access_token: ::std::option::Option<&str>,
+                ) -> ::std::result::Result<
+                    ::ruma_api::exports::http::Request<Vec<u8>>,
+                    ::ruma_api::error::IntoHttpError,
+                > {
+                    let metadata = Request::METADATA;
+
+                    let http_request = ::ruma_api::exports::http::Request::builder()
+                        .method(::ruma_api::exports::http::Method::#method)
+                        .uri(::std::format!(
+                            "{}{}{}",
+                            base_url.strip_suffix("/").unwrap_or(base_url),
+                            #request_path_string,
+                            #request_query_string,
+                        ))
+                        #( .header(#header_kvs) )*
+                        .body(#request_body)?;
+
+                    Ok(http_request)
+                }
             }
 
             #non_auth_endpoint_impl
diff --git a/ruma-api-macros/src/api/request.rs b/ruma-api-macros/src/api/request.rs
index c60c6cc0..f694a865 100644
--- a/ruma-api-macros/src/api/request.rs
+++ b/ruma-api-macros/src/api/request.rs
@@ -22,8 +22,8 @@ pub struct Request {
 
 impl Request {
     /// Produces code to add necessary HTTP headers to an `http::Request`.
-    pub fn add_headers_to_request(&self) -> TokenStream {
-        let append_stmts = self.header_fields().map(|request_field| {
+    pub fn append_header_kvs(&self) -> Vec<TokenStream> {
+        self.header_fields().map(|request_field| {
             let (field, header_name) = match request_field {
                 RequestField::Header(field, header_name) => (field, header_name),
                 _ => unreachable!("expected request field to be header variant"),
@@ -32,16 +32,10 @@ impl Request {
             let field_name = &field.ident;
 
             quote! {
-                headers.append(
-                    ::ruma_api::exports::http::header::#header_name,
-                    ::ruma_api::exports::http::header::HeaderValue::from_str(request.#field_name.as_ref())?,
-                );
+                ::ruma_api::exports::http::header::#header_name,
+                ::ruma_api::exports::http::header::HeaderValue::from_str(self.#field_name.as_ref())?
             }
-        });
-
-        quote! {
-            #(#append_stmts)*
-        }
+        }).collect()
     }
 
     /// Produces code to extract fields from the HTTP headers in an `http::Request`.
@@ -136,12 +130,12 @@ impl Request {
 
     /// Produces code for a struct initializer for body fields on a variable named `request`.
     pub fn request_body_init_fields(&self) -> TokenStream {
-        self.struct_init_fields(RequestFieldKind::Body, quote!(request))
+        self.struct_init_fields(RequestFieldKind::Body, quote!(self))
     }
 
     /// Produces code for a struct initializer for query string fields on a variable named `request`.
     pub fn request_query_init_fields(&self) -> TokenStream {
-        self.struct_init_fields(RequestFieldKind::Query, quote!(request))
+        self.struct_init_fields(RequestFieldKind::Query, quote!(self))
     }
 
     /// Produces code for a struct initializer for body fields on a variable named `request_body`.
diff --git a/ruma-api-macros/src/derive_outgoing.rs b/ruma-api-macros/src/derive_outgoing.rs
index 1364247d..0014bad4 100644
--- a/ruma-api-macros/src/derive_outgoing.rs
+++ b/ruma-api-macros/src/derive_outgoing.rs
@@ -138,10 +138,10 @@ fn strip_lifetimes(field_type: &mut Type) -> bool {
 
                 if path.is_ident("str") {
                     // &str -> String
-                    *field_type = parse_quote! { String };
+                    *field_type = parse_quote! { ::std::string::String };
                 } else if segs.contains(&"DeviceId".into()) || segs.contains(&"ServerName".into()) {
                     // The identifiers that need to be boxed `Box<T>` since they are DST's.
-                    *field_type = parse_quote! { Box<#path> };
+                    *field_type = parse_quote! { ::std::boxed::Box<#path> };
                 } else {
                     // &T -> T
                     *field_type = Type::Path(ty_path.clone());
diff --git a/ruma-api-macros/src/util.rs b/ruma-api-macros/src/util.rs
index ba13cd47..20d6c7ea 100644
--- a/ruma-api-macros/src/util.rs
+++ b/ruma-api-macros/src/util.rs
@@ -12,7 +12,8 @@ use crate::api::{metadata::Metadata, request::Request};
 /// of the path that start with ":".
 ///
 /// The first `TokenStream` returned is the constructed url path. The second `TokenStream` is
-/// used for implementing `TryFrom<http::Request<Vec<u8>>>`, from path strings deserialized to ruma types.
+/// used for implementing `TryFrom<http::Request<Vec<u8>>>`, from path strings deserialized to Ruma
+/// types.
 pub(crate) fn request_path_string_and_parse(
     request: &Request,
     metadata: &Metadata,
@@ -44,16 +45,16 @@ pub(crate) fn request_path_string_and_parse(
                     Span::call_site(),
                 );
                 format_args.push(quote! {
-                    ruma_api::exports::percent_encoding::utf8_percent_encode(
-                        &request.#path_var.to_string(),
-                        ruma_api::exports::percent_encoding::NON_ALPHANUMERIC,
+                    ::ruma_api::exports::percent_encoding::utf8_percent_encode(
+                        &self.#path_var.to_string(),
+                        ::ruma_api::exports::percent_encoding::NON_ALPHANUMERIC,
                     )
                 });
                 format_string.replace_range(start_of_segment..end_of_segment, "{}");
             }
 
             quote! {
-                format!(#format_string, #(#format_args),*)
+                format_args!(#format_string, #(#format_args),*)
             }
         };
 
@@ -64,17 +65,16 @@ pub(crate) fn request_path_string_and_parse(
                     let path_var_ident = Ident::new(path_var, Span::call_site());
                     quote! {
                         #path_var_ident: {
-                            use std::ops::Deref as _;
-                            use ruma_api::error::RequestDeserializationError;
+                            use ::ruma_api::error::RequestDeserializationError;
 
                             let segment = path_segments.get(#i).unwrap().as_bytes();
                             let decoded = ::ruma_api::try_deserialize!(
                                 request,
-                                ruma_api::exports::percent_encoding::percent_decode(segment)
+                                ::ruma_api::exports::percent_encoding::percent_decode(segment)
                                     .decode_utf8(),
                             );
 
-                            ::ruma_api::try_deserialize!(request, std::convert::TryFrom::try_from(decoded.deref()))
+                            ::ruma_api::try_deserialize!(request, ::std::convert::TryFrom::try_from(&*decoded))
                         }
                     }
                 },
@@ -106,12 +106,15 @@ pub(crate) fn build_query_string(request: &Request) -> TokenStream {
             // ensure that it won't fail.
             fn assert_trait_impl<T>()
             where
-                T: std::iter::IntoIterator<Item = (std::string::String, std::string::String)>,
+                T: ::std::iter::IntoIterator<Item = (::std::string::String, ::std::string::String)>,
             {}
             assert_trait_impl::<#field_type>();
 
-            let request_query = RequestQuery(request.#field_name);
-            format!("?{}", ruma_api::exports::ruma_serde::urlencoded::to_string(request_query)?)
+            let request_query = RequestQuery(self.#field_name);
+            format_args!(
+                "?{}",
+                ::ruma_api::exports::ruma_serde::urlencoded::to_string(request_query)?
+            )
         })
     } else if request.has_query_fields() {
         let request_query_init_fields = request.request_query_init_fields();
@@ -121,12 +124,13 @@ pub(crate) fn build_query_string(request: &Request) -> TokenStream {
                 #request_query_init_fields
             };
 
-            format!("?{}", ruma_api::exports::ruma_serde::urlencoded::to_string(request_query)?)
+            format_args!(
+                "?{}",
+                ::ruma_api::exports::ruma_serde::urlencoded::to_string(request_query)?
+            )
         })
     } else {
-        quote! {
-            String::new()
-        }
+        quote! { "" }
     }
 }
 
@@ -161,11 +165,11 @@ pub(crate) fn extract_request_query(request: &Request) -> TokenStream {
 pub(crate) fn build_request_body(request: &Request) -> TokenStream {
     if let Some(field) = request.newtype_raw_body_field() {
         let field_name = field.ident.as_ref().expect("expected field to have an identifier");
-        quote!(request.#field_name)
+        quote!(self.#field_name)
     } else if request.has_body_fields() || request.newtype_body_field().is_some() {
         let request_body_initializers = if let Some(field) = request.newtype_body_field() {
             let field_name = field.ident.as_ref().expect("expected field to have an identifier");
-            quote! { (request.#field_name) }
+            quote! { (self.#field_name) }
         } else {
             let initializers = request.request_body_init_fields();
             quote! { { #initializers } }
@@ -174,7 +178,7 @@ pub(crate) fn build_request_body(request: &Request) -> TokenStream {
         quote! {
             {
                 let request_body = RequestBody #request_body_initializers;
-                ruma_api::exports::serde_json::to_vec(&request_body)?
+                ::ruma_api::exports::serde_json::to_vec(&request_body)?
             }
         }
     } else {
diff --git a/ruma-api/src/error.rs b/ruma-api/src/error.rs
index 2d7cf6ff..95223fe1 100644
--- a/ruma-api/src/error.rs
+++ b/ruma-api/src/error.rs
@@ -16,40 +16,62 @@ impl crate::EndpointError for Void {
         Err(ResponseDeserializationError::from_response(response))
     }
 }
+
 /// An error when converting one of ruma's endpoint-specific request or response
 /// types to the corresponding http type.
 #[derive(Debug)]
-pub struct IntoHttpError(SerializationError);
+pub struct IntoHttpError(InnerIntoHttpError);
+
+impl IntoHttpError {
+    // For usage in macros
+    #[doc(hidden)]
+    pub fn needs_authentication() -> Self {
+        Self(InnerIntoHttpError::NeedsAuthentication)
+    }
+}
 
 #[doc(hidden)]
 impl From<serde_json::Error> for IntoHttpError {
     fn from(err: serde_json::Error) -> Self {
-        Self(SerializationError::Json(err))
+        Self(InnerIntoHttpError::Json(err))
     }
 }
 
 #[doc(hidden)]
 impl From<ruma_serde::urlencoded::ser::Error> for IntoHttpError {
     fn from(err: ruma_serde::urlencoded::ser::Error) -> Self {
-        Self(SerializationError::Query(err))
+        Self(InnerIntoHttpError::Query(err))
     }
 }
 
 #[doc(hidden)]
 impl From<http::header::InvalidHeaderValue> for IntoHttpError {
     fn from(err: http::header::InvalidHeaderValue) -> Self {
-        Self(SerializationError::Header(err))
+        Self(InnerIntoHttpError::Header(err))
+    }
+}
+
+#[doc(hidden)]
+impl From<http::Error> for IntoHttpError {
+    fn from(err: http::Error) -> Self {
+        Self(InnerIntoHttpError::Http(err))
     }
 }
 
 impl Display for IntoHttpError {
     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
         match &self.0 {
-            SerializationError::Json(err) => write!(f, "JSON serialization failed: {}", err),
-            SerializationError::Query(err) => {
+            InnerIntoHttpError::NeedsAuthentication => write!(
+                f,
+                "This endpoint has to be converted to http::Request using \
+                try_into_authenticated_http_request"
+            ),
+            InnerIntoHttpError::Json(err) => write!(f, "JSON serialization failed: {}", err),
+            InnerIntoHttpError::Query(err) => {
                 write!(f, "Query parameter serialization failed: {}", err)
             }
-            SerializationError::Header(err) => write!(f, "Header serialization failed: {}", err),
+            InnerIntoHttpError::Header(err) => write!(f, "Header serialization failed: {}", err),
+            InnerIntoHttpError::Http(err) => write!(f, "HTTP request construction failed: {}", err),
         }
     }
 }
@@ -201,10 +223,12 @@ impl<E: Display> Display for ServerError<E> {
 impl<E: std::error::Error> std::error::Error for ServerError<E> {}
 
 #[derive(Debug)]
-enum SerializationError {
+enum InnerIntoHttpError {
+    NeedsAuthentication,
     Json(serde_json::Error),
     Query(ruma_serde::urlencoded::ser::Error),
     Header(http::header::InvalidHeaderValue),
+    Http(http::Error),
 }
 
 /// This type is public so it is accessible from `ruma_api!` generated code.
diff --git a/ruma-api/src/lib.rs b/ruma-api/src/lib.rs
index aba96d46..67297aa7 100644
--- a/ruma-api/src/lib.rs
+++ b/ruma-api/src/lib.rs
@@ -41,7 +41,7 @@ use http::Method;
 ///         // Struct fields for each piece of data expected
 ///         // in the response from this API endpoint.
 ///     }
-///     
+///
 ///     // The error returned when a response fails, defaults to `Void`.
 ///     error: path::to::Error
 /// }
@@ -245,7 +245,7 @@ pub trait EndpointError: Sized {
 /// A Matrix API endpoint.
 ///
 /// The type implementing this trait contains any data needed to make a request to the endpoint.
-pub trait Endpoint: Outgoing + TryInto<http::Request<Vec<u8>>, Error = IntoHttpError>
+pub trait Endpoint: Outgoing
 where
     <Self as Outgoing>::Incoming: TryFrom<http::Request<Vec<u8>>, Error = FromHttpRequestError>,
     <Self::Response as Outgoing>::Incoming: TryFrom<
@@ -260,6 +260,20 @@ where
 
     /// Metadata about the endpoint.
     const METADATA: Metadata;
+
+    /// Tries to convert this request into an `http::Request`.
+    ///
+    /// This method should only fail when called on endpoints that require authentication. It may
+    /// also fail with a serialization error in case of bugs in Ruma though.
+    ///
+    /// The endpoints path will be appended to the given `base_url`, for example
+    /// `https://matrix.org`. Since all paths begin with a slash, it is not necessary for the
+    /// `base_url` to have a trailing slash. If it has one however, it will be ignored.
+    fn try_into_http_request(
+        self,
+        base_url: &str,
+        access_token: Option<&str>,
+    ) -> Result<http::Request<Vec<u8>>, IntoHttpError>;
 }
 
 /// A Matrix API endpoint that doesn't require authentication.
@@ -357,26 +371,24 @@ mod tests {
                 name: "create_alias",
                 path: "/_matrix/client/r0/directory/room/:room_alias",
                 rate_limited: false,
-                requires_authentication: true,
+                requires_authentication: false,
             };
-        }
-
-        impl TryFrom<Request> for http::Request<Vec<u8>> {
-            type Error = IntoHttpError;
 
-            fn try_from(request: Request) -> Result<http::Request<Vec<u8>>, Self::Error> {
+            fn try_into_http_request(
+                self,
+                base_url: &str,
+                _access_token: Option<&str>,
+            ) -> Result<http::Request<Vec<u8>>, IntoHttpError> {
                 let metadata = Request::METADATA;
 
-                let path = metadata
-                    .path
-                    .to_string()
-                    .replace(":room_alias", &request.room_alias.to_string());
+                let url = (base_url.to_owned() + metadata.path)
+                    .replace(":room_alias", &self.room_alias.to_string());
 
-                let request_body = RequestBody { room_id: request.room_id };
+                let request_body = RequestBody { room_id: self.room_id };
 
                 let http_request = http::Request::builder()
                     .method(metadata.method)
-                    .uri(path)
+                    .uri(url)
                     .body(serde_json::to_vec(&request_body)?)
                     // this cannot fail because we don't give user-supplied data to any of the
                     // builder methods
diff --git a/ruma-client-api/src/r0/message/get_message_events.rs b/ruma-client-api/src/r0/message/get_message_events.rs
index e09cb9f2..bcae9b72 100644
--- a/ruma-client-api/src/r0/message/get_message_events.rs
+++ b/ruma-client-api/src/r0/message/get_message_events.rs
@@ -117,9 +117,10 @@ pub enum Direction {
 mod tests {
     use super::{Direction, Request};
 
-    use std::convert::{TryFrom, TryInto};
+    use std::convert::TryFrom;
 
     use js_int::uint;
+    use ruma_api::Endpoint;
     use ruma_identifiers::RoomId;
 
     use crate::r0::filter::{LazyLoadOptions, RoomEventFilter};
@@ -143,7 +144,8 @@ mod tests {
             filter: Some(filter),
         };
 
-        let request: http::Request<Vec<u8>> = req.try_into().unwrap();
+        let request: http::Request<Vec<u8>> =
+            req.try_into_http_request("https://homeserver.tld", Some("auth_tok")).unwrap();
         assert_eq!(
             "from=token&to=token2&dir=b&limit=0&filter=%7B%22not_types%22%3A%5B%22type%22%5D%2C%22not_rooms%22%3A%5B%22room%22%2C%22room2%22%2C%22room3%22%5D%2C%22rooms%22%3A%5B%22%21roomid%3Aexample.org%22%5D%2C%22lazy_load_members%22%3Atrue%2C%22include_redundant_members%22%3Atrue%7D",
             request.uri().query().unwrap()
@@ -162,7 +164,8 @@ mod tests {
             filter: None,
         };
 
-        let request: http::Request<Vec<u8>> = req.try_into().unwrap();
+        let request =
+            req.try_into_http_request("https://homeserver.tld", Some("auth_tok")).unwrap();
         assert_eq!("from=token&to=token2&dir=b&limit=0", request.uri().query().unwrap(),);
     }
 
@@ -178,7 +181,8 @@ mod tests {
             filter: Some(RoomEventFilter::default()),
         };
 
-        let request: http::Request<Vec<u8>> = req.try_into().unwrap();
+        let request: http::Request<Vec<u8>> =
+            req.try_into_http_request("https://homeserver.tld", Some("auth_tok")).unwrap();
         assert_eq!(
             "from=token&to=token2&dir=b&limit=0&filter=%7B%7D",
             request.uri().query().unwrap(),
diff --git a/ruma-client-api/src/r0/session/login.rs b/ruma-client-api/src/r0/session/login.rs
index 552dd4b0..623a5763 100644
--- a/ruma-client-api/src/r0/session/login.rs
+++ b/ruma-client-api/src/r0/session/login.rs
@@ -141,8 +141,7 @@ mod user_serde;
 
 #[cfg(test)]
 mod tests {
-    use std::convert::TryInto;
-
+    use ruma_api::Endpoint;
     use serde_json::{from_value as from_json_value, json, Value as JsonValue};
 
     use super::{LoginInfo, Medium, Request, UserInfo};
@@ -193,7 +192,7 @@ mod tests {
             device_id: None,
             initial_device_display_name: Some("test".into()),
         }
-        .try_into()
+        .try_into_http_request("https://homeserver.tld", None)
         .unwrap();
 
         let req_body_value: JsonValue = serde_json::from_slice(req.body()).unwrap();
diff --git a/ruma-client-api/src/r0/sync/sync_events.rs b/ruma-client-api/src/r0/sync/sync_events.rs
index fb389273..e0a28dfa 100644
--- a/ruma-client-api/src/r0/sync/sync_events.rs
+++ b/ruma-client-api/src/r0/sync/sync_events.rs
@@ -403,6 +403,7 @@ impl DeviceLists {
 mod tests {
     use std::{convert::TryInto, time::Duration};
 
+    use ruma_api::Endpoint;
     use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
 
     use matches::assert_matches;
@@ -418,7 +419,7 @@ mod tests {
             set_presence: PresenceState::Offline,
             timeout: Some(Duration::from_millis(30000)),
         }
-        .try_into()
+        .try_into_http_request("https://homeserver.tld", Some("auth_tok"))
         .unwrap();
 
         let uri = req.uri();


Test patch:
diff --git a/ruma-api/tests/conversions.rs b/ruma-api/tests/conversions.rs
index b9094df9..c8c598b0 100644
--- a/ruma-api/tests/conversions.rs
+++ b/ruma-api/tests/conversions.rs
@@ -47,7 +47,7 @@ fn request_serde() -> Result<(), Box<dyn std::error::Error + 'static>> {
         baz: UserId::try_from("@bazme:ruma.io")?,
     };
 
-    let http_req = http::Request::<Vec<u8>>::try_from(req.clone())?;
+    let http_req = req.clone().try_into_http_request("https://homeserver.tld", None)?;
     let req2 = Request::try_from(http_req)?;
 
     assert_eq!(req.hello, req2.hello);
diff --git a/ruma-api/tests/no_fields.rs b/ruma-api/tests/no_fields.rs
index db131af2..38ce16d0 100644
--- a/ruma-api/tests/no_fields.rs
+++ b/ruma-api/tests/no_fields.rs
@@ -1,6 +1,6 @@
 use std::convert::TryFrom;
 
-use ruma_api::ruma_api;
+use ruma_api::{ruma_api, Endpoint};
 
 ruma_api! {
     metadata: {
@@ -19,7 +19,7 @@ ruma_api! {
 #[test]
 fn empty_request_http_repr() {
     let req = Request {};
-    let http_req = http::Request::<Vec<u8>>::try_from(req).unwrap();
+    let http_req = req.try_into_http_request("https://homeserver.tld", None).unwrap();
 
     assert!(http_req.body().is_empty());
 }