Repository: denoland/deno (language: rust)

Issue:
Wrong path when using HTTP proxy in Node API `http`
Version: Deno 2.1.5

TLDR: when `http.request`'s opinions include proxy settings and `options.path` is a full URL (e.g., `http://httpbin.org/200`), the website that proxy server accesses should be `http://httpbin.org/200`, but in Deno it is `http://httpbin.org/http://httpbin.org/200`.

POC:
```js
import axios from "axios";
const instance = axios.create({
  baseURL: "http://httpbin.org/",
  proxy: {
    protocol: "http",
    host: "127.0.0.1",
    port: 8892,
  },
});
try {
  const resp = await instance.get("/get");
  console.log("success", resp.config);
} catch (e) {
  console.log(e);
}
```

For convenience, I use Axios as demo, and related code is [here](https://github.com/axios/axios/blob/bad6d8b97b52c0c15311c92dd596fc0bff122651/lib/adapters/http.js#L464C11-L464C28). When I add a patch `options.path = new URL(options.path).pathname;`, it becomes correct.
This demo requires a http proxy running at port 8892 (such as Fiddler or mitmproxy).

In Node.JS, it can get `http://httpbin.org/get` correctly. In Deno, it gets a 404, while Fiddler shows it is requesting `http://httpbin.org/http://httpbin.org/get`


Discussion / hints:

Fix patch:
diff --git a/ext/node/ops/http.rs b/ext/node/ops/http.rs
index 57bcf69a47cf3c..1da630f97edebd 100644
--- a/ext/node/ops/http.rs
+++ b/ext/node/ops/http.rs
@@ -113,6 +113,9 @@ pub enum ConnError {
   #[error("Invalid URL {0}")]
   InvalidUrl(Url),
   #[class(type)]
+  #[error("Invalid Path {0}")]
+  InvalidPath(String),
+  #[class(type)]
   #[error(transparent)]
   InvalidHeaderName(#[from] http::header::InvalidHeaderName),
   #[class(type)]
@@ -150,6 +153,7 @@ pub async fn op_node_http_request_with_conn<P>(
   state: Rc<RefCell<OpState>>,
   #[serde] method: ByteString,
   #[string] url: String,
+  #[string] request_path: Option<String>,
   #[serde] headers: Vec<(ByteString, ByteString)>,
   #[smi] body: Option<ResourceId>,
   #[smi] conn_rid: ResourceId,
@@ -247,11 +251,17 @@ where
   *request.method_mut() = method.clone();
   let path = url_parsed.path();
   let query = url_parsed.query();
-  *request.uri_mut() = query
-    .map(|q| format!("{}?{}", path, q))
-    .unwrap_or_else(|| path.to_string())
-    .parse()
-    .map_err(|_| ConnError::InvalidUrl(url_parsed.clone()))?;
+  if let Some(request_path) = request_path {
+    *request.uri_mut() = request_path
+      .parse()
+      .map_err(|_| ConnError::InvalidPath(request_path.clone()))?;
+  } else {
+    *request.uri_mut() = query
+      .map(|q| format!("{}?{}", path, q))
+      .unwrap_or_else(|| path.to_string())
+      .parse()
+      .map_err(|_| ConnError::InvalidUrl(url_parsed.clone()))?;
+  }
   *request.headers_mut() = header_map;
 
   if let Some((username, password)) = maybe_authority {
diff --git a/ext/node/polyfills/http.ts b/ext/node/polyfills/http.ts
index dd94c9d025fd95..0438f9af22e6c7 100644
--- a/ext/node/polyfills/http.ts
+++ b/ext/node/polyfills/http.ts
@@ -479,6 +479,7 @@ class ClientRequest extends OutgoingMessage {
         this._req = await op_node_http_request_with_conn(
           this.method,
           url,
+          this._createRequestPath(),
           headers,
           this._bodyWriteRid,
           baseConnRid,
@@ -817,6 +818,15 @@ class ClientRequest extends OutgoingMessage {
     return url.href;
   }
 
+  _createRequestPath(): string | undefined {
+    // If the path starts with protocol, pass this to op_node_http_request_with_conn
+    // This will be used as Request.uri in hyper for supporting http proxy
+    if (this.path?.startsWith("http://") || this.path?.startsWith("https://")) {
+      return this.path;
+    }
+    return undefined;
+  }
+
   setTimeout(msecs: number, callback?: () => void) {
     if (msecs === 0) {
       if (this._timeout) {


Test patch:
diff --git a/tests/unit_node/http_test.ts b/tests/unit_node/http_test.ts
index b4f0d260aa0906..54803ab995f511 100644
--- a/tests/unit_node/http_test.ts
+++ b/tests/unit_node/http_test.ts
@@ -1892,3 +1892,27 @@ Deno.test("[node/http] an error with DNS propagates to request object", async ()
   });
   await promise;
 });
+
+Deno.test("[node/http] supports proxy http request", async () => {
+  const { promise, resolve } = Promise.withResolvers<void>();
+  const server = Deno.serve({ port: 0, onListen }, (req) => {
+    console.log("server received", req.url);
+    assertEquals(req.url, "http://example.com/");
+    return new Response("ok");
+  });
+
+  function onListen({ port }: { port: number }) {
+    http.request({
+      host: "localhost",
+      port,
+      path: "http://example.com",
+    }, async (res) => {
+      assertEquals(res.statusCode, 200);
+      assertEquals(await text(res), "ok");
+      resolve();
+      server.shutdown();
+    }).end();
+  }
+  await promise;
+  await server.finished;
+});