Search code examples
javascriptnode.jsproxyundici

Node.js fetch with ProxyAgent from undici never gets response (axios works)


I want in a Node.js project to perform a simple fetch request with proxy by specifically using ProxyAgent from undici and native fetch but i can't get it to work. The request never resolves or rejects and the first console.log of the proxy code never runs. Other ways like using axios or http.get or even postman do work. Am i doing something wrong? Does undici proxy for some reason works only with certain proxies?

index.js

import { ProxyAgent } from "undici";

const client = new ProxyAgent("http://127.0.0.1:8000");

fetch("http://ifconfig.io/ip", {
  dispatcher: client,
  method: "GET",
})
  .then((response) => {
    console.log("Got response");
    if (!response.ok) {
      throw Error("response not ok");
    }
    return response.text();
  })
  .then((data) => {
    console.log("Response Data = ", data);
  })
  .catch((error) => {
    console.error("Error occurred:", error.message);
  });

Code of a simple proxy with Node.js (which works fine with postman and axios) proxy.js

const http = require('http');
const httpProxy = require('http-proxy');
const url = require('url');

// Create a proxy server
const proxy = httpProxy.createProxyServer({});

// Create an HTTP server that will use the proxy
const server = http.createServer((req, res) => {
  console.log("Original request URL:", req.url);
  
  const parsedUrl = url.parse(req.url, true);
  // The target is set directly to the desired backend service
  const target = `${parsedUrl.protocol}//${parsedUrl.host}`; 

  // Proxy the request to the target server
  proxy.web(req, res, { target });

  // Log the proxied request
  console.log(`Proxied request to: ${target}`);
});

// Listen on a specific port (e.g., 8000)
const PORT = 8000;
server.listen(PORT, () => {
  console.log(`Dynamic proxy server is running on http://localhost:${PORT}`);
});

Tried to also import fetch from undici but same results. In case it helps i run this in Node.js LTS version 20.17.0 (also tried in 18.18.2) and this my package.json

{
  "name": "nodejs-proj",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type":"module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "axios": "^1.7.4",
    "http-proxy": "^1.18.1",
    "http-proxy-agent": "^7.0.2",
    "undici": "^6.19.8"
  }
}

Working example with axios. indexAxios.js

import axios from "axios";

axios.get('https://ifconfig.io/ip', {
  proxy: {
    protocol: "http",
    host: '127.0.0.1',
    port: 8000,
  }
})
  .then(response => {
    console.log('Response data:', response.data);
  })
  .catch(error => {
    console.error('Error occurred:', error.message);
  });

Also tried this proxy same results

import http from "http";
import request from "request";

const PORT = 8000;

http.createServer(function (req, res) {
  console.log(`Incoming request to ${req.url}`);
  req.pipe(request(req.url)).pipe(res);
}).listen(PORT);

Also tried the following proxy which again works just fine with axios but when i use it with the fetch example it just bypasses it and gets a response. (fetched data from an http mock api because it didn't like https)

import * as http from 'http';
import { createProxy } from 'proxy';

const server = createProxy(http.createServer());

// Log each incoming request
server.on('request', (req, res) => {
  console.log(`Incoming request: ${req.method} ${req.url}`);
});

server.listen(3128, () => {
  var port = server.address().port;
  console.log('HTTP(s) proxy server listening on port %d', port);
});

Solution

  • Do you want to make an HTTP request (like in index.js) or an HTTPS request (like in indexAxios.js)? The following answer is about HTTPS requests.

    Behavior of fetch

    HTTP and HTTPS requests made by fetch are much different when proxies are involved.

    If the fetch statement in your index.js makes an HTTPS request through a proxy, it first sends a CONNECT request

    CONNECT ifconfig.io:443 HTTP/1.1
    Host: ifconfig.io:443
    Proxy-Connection: Keep-Alive
    

    to http://localhost:8000.

    But your proxy.js server is not prepared to handle such CONNECT requests. It must contain an event handler like

    server.on("connect", function(request, socket, head) {
      // request.url = "ifconfig.io:443"
      var s = net.createConnection({host: "ifconfig.io", port: 443}, function() {
        socket.write("HTTP/1.1 200\r\n\r\n");
        s.pipe(socket);
        s.write(head);
      });
      socket.once("data", function(data) {
        s.write(data);
        socket.pipe(s);
      });
    });
    

    The handler establishes a connection with the target host and forwards all subsequent (encrypted) traffic that goes over the same connection ("SSL tunneling").

    The other proxies you tried seem to lack such a CONNECT handler as well (what you call "it didn't like https").

    Behavior of axios

    axios does not use SSL tunneling. Instead of a CONNECT request it makes the following request (unencrypted) to http://localhost:8080:

    GET https://ifconfig.io/ip HTTP/1.1
    host: ifconfig.io
    Connection: keep-alive
    

    Your proxy.js server then forwards this as a separate HTTPS request, but since the request from browser to proxy is HTTP only, the request made by axios is not end-to-end encrypted. For example, the "path portion" of the URL, (/ip) is transmitted unencrypted between browser and proxy and so is the response to this request.

    (By contrast, the unencrypted CONNECT request does not contain the entire URL, only the host.)