Repository: actix/actix-web (language: rust)

Issue:
Request.uri() returns the Path instead of the full URL
## Expected Behavior

I need the full URL in a response handler.
`Request.uri()` should print the full URL (e.g. `https://example.com/hello-world`).

## Current Behavior

Prints the path (`/hello-world`), which is _not_ a [valid URI](https://www.rfc-editor.org/rfc/rfc3986).

## Possible Solution

Suggestion:

- The [`URI` struct](https://docs.rs/actix-web/latest/actix_web/http/struct.Uri.html) should always be a valid URI, which means it must have a schema + host + origin.
- The current behavior of `req.uri()` returns the `path` of the URI. I think a `.path()` method is a better name for this. Add a `full_uri()` method that returns the full URI.

## Steps to Reproduce (for bugs)

```rust
pub async fn handle_get_resource(
    req: actix_web::HttpRequest,
) -> AtomicServerResult<HttpResponse> {
    println!("URL: {}", req.uri());
```

## Your Environment
<!--- Include as many relevant details about the environment you experienced the bug in -->

- Rust Version (I.e, output of `rustc -V`): rustc 1.63.0 (4b91a6ea7 2022-08-08)
- Actix Web Version: 4.1.0



Discussion / hints:

Fix patch:
diff --git a/actix-web/CHANGES.md b/actix-web/CHANGES.md
index 28dd25fb5b3..bb0844e0591 100644
--- a/actix-web/CHANGES.md
+++ b/actix-web/CHANGES.md
@@ -2,6 +2,10 @@
 
 ## Unreleased
 
+### Added
+
+- Add `HttpRequest::full_url()` method to get the complete URL of the request.
+
 ### Fixed
 
 - `ConnectionInfo::realip_remote_addr()` now handles IPv6 addresses from `Forwarded` header correctly. Previously, it sometimes returned the forwarded port as well.
diff --git a/actix-web/src/request.rs b/actix-web/src/request.rs
index 08a222c866f..47b3e3d881c 100644
--- a/actix-web/src/request.rs
+++ b/actix-web/src/request.rs
@@ -91,6 +91,35 @@ impl HttpRequest {
         &self.head().uri
     }
 
+    /// Returns request's original full URL.
+    ///
+    /// Reconstructed URL is best-effort, using [`connection_info`](HttpRequest::connection_info())
+    /// to get forwarded scheme & host.
+    ///
+    /// ```
+    /// use actix_web::test::TestRequest;
+    /// let req = TestRequest::with_uri("http://10.1.2.3:8443/api?id=4&name=foo")
+    ///     .insert_header(("host", "example.com"))
+    ///     .to_http_request();
+    ///
+    /// assert_eq!(
+    ///     req.full_url().as_str(),
+    ///     "http://example.com/api?id=4&name=foo",
+    /// );
+    /// ```
+    pub fn full_url(&self) -> url::Url {
+        let info = self.connection_info();
+        let scheme = info.scheme();
+        let host = info.host();
+        let path_and_query = self
+            .uri()
+            .path_and_query()
+            .map(|paq| paq.as_str())
+            .unwrap_or("/");
+
+        url::Url::parse(&format!("{scheme}://{host}{path_and_query}")).unwrap()
+    }
+
     /// Read the Request method.
     #[inline]
     pub fn method(&self) -> &Method {
@@ -963,4 +992,27 @@ mod tests {
 
         assert!(format!("{:?}", req).contains(location_header));
     }
+
+    #[test]
+    fn check_full_url() {
+        let req = TestRequest::with_uri("/api?id=4&name=foo").to_http_request();
+        assert_eq!(
+            req.full_url().as_str(),
+            "http://localhost:8080/api?id=4&name=foo",
+        );
+
+        let req = TestRequest::with_uri("https://example.com/api?id=4&name=foo").to_http_request();
+        assert_eq!(
+            req.full_url().as_str(),
+            "https://example.com/api?id=4&name=foo",
+        );
+
+        let req = TestRequest::with_uri("http://10.1.2.3:8443/api?id=4&name=foo")
+            .insert_header(("host", "example.com"))
+            .to_http_request();
+        assert_eq!(
+            req.full_url().as_str(),
+            "http://example.com/api?id=4&name=foo",
+        );
+    }
 }


Test patch: