Repository: denoland/deno
Pull request: fix(ext/node): support proxy http request

closes #27692

This PR add support of proxy http request to `node:http.request`.

Passing the full url in `path` seems causing proxy http request in Node.js (ref: https://stackoverflow.com/questions/3862813/how-can-i-use-an-http-proxy-with-node-js-http-client ), and `axios` seems using this feature as pointed in #27692 (strangely I couldn't find a native test case that tests this feature though..)

This PR enhances `op_node_http_request_with_conn` to accept extra `request_path`. If it's passed, it overwrites the default uri of `Request` struct and causes proxy request to the server.

Changed files:
filename: ext/node/ops/http.rs
patch: @@ -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 {

filename: ext/node/polyfills/http.ts
patch: @@ -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) {

filename: tests/unit_node/http_test.ts
patch: @@ -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;
+});