Skip to content

ProxyAgent has different behavior from curl -x #4083

Closed

Description

@joyeecheung

Bug Description

When testing nodejs/node#57165 I noticed that there are some behavior differences between ProxyAgent and curl which may be interesting. Most notably, ProxyAgent always tunnels with CONNECT even for pure HTTP traffic.

Reproducible By

Create a proxy server, like this:

// server.js
'use strict';

const http = require('http');
const net = require('net');

function log(req) {
  console.log('----- Received Request -----');
  console.log(`${req.method} ${req.url} HTTP/${req.httpVersion}`);
  for (const header in req.headers) {
    console.log(`${header}: ${req.headers[header]}`);
  }
}

const server = http.createServer((req, res) => {
  log(req);
  const [hostname, port] = req.headers.host.split(':');
  const targetPort = port || 80;

  const options = {
    hostname: hostname,
    port: targetPort,
    path: req.url,
    method: req.method,
    headers: req.headers
  };

  const proxyReq = http.request(options, proxyRes => {
    res.writeHead(proxyRes.statusCode, proxyRes.headers);
    proxyRes.pipe(res, { end: true });
  });

  proxyReq.on('error', err => {
    console.error('Proxy request error: ' + err.message);
    res.writeHead(500);
    res.end('Proxy error: ' + err.message);
  });

  req.pipe(proxyReq, { end: true });
});

server.on('connect', function (req, clientSocket, head) {
  log(req);
  const [hostname, port] = req.url.split(':');

  const serverSocket = net.connect(port, hostname, () => {
    clientSocket.write(
      'HTTP/1.1 200 Connection Established\r\n' +
      'Proxy-agent: Node.js-Proxy\r\n' +
      '\r\n'
    );
    serverSocket.write(head);
    clientSocket.pipe(serverSocket);
    serverSocket.pipe(clientSocket);
  });

  serverSocket.on('error', (err) => {
    console.error('Error on CONNECT tunnel:', err.message);
    clientSocket.write('HTTP/1.1 500 Connection Error\r\n\r\n');
    clientSocket.end();
  });
});

server.listen(8000, '127.0.0.1', () => {
  const { address, port } = server.address();
  console.log(`Proxy server listening on http://${address}:${port}`);
});

And send a request to http://example.com like this:

// proxy
const { fetch, ProxyAgent } = require('undici');

(async () => {
  const res = await fetch('http://example.com', {
    dispatcher: new ProxyAgent('http://127.0.0.1:8000')
  });
  console.log(await res.text());
})();

Expected Behavior

If I run curl -x http://127.0.0.1:8000 http://example.com, this is the output from the proxy server:

GET http://example.com/ HTTP/1.1
host: example.com
user-agent: curl/8.7.1
accept: */*
proxy-connection: Keep-Alive

Logs & Screenshots

Using the client with undici, the proxy server always get a CONNECT:

CONNECT example.com:80 HTTP/1.1
host: example.com
connection: close

This is somewhat similar to the one I get for sending requests to https://example.com using curl - which does use CONNECT.

Output from curl -x http://127.0.0.1:8000 https://example.com

CONNECT example.com:443 HTTP/1.1
host: example.com:443
user-agent: curl/8.7.1
proxy-connection: Keep-Alive

Output from client with undici ProxyAgent (change the http://example.com to https://example.com in the snippet above)

CONNECT example.com:80 HTTP/1.1
host: example.com
connection: close

Environment

macOS though I am sure this is irrelevant.

Additional context

I am not sure this really counts as a bug or not. But from my impression, not all proxy servers in the wild supports tunneling on non-443 ports. It may be safer to follow what curl does, because the setup of proxy server and target servers are not always in the control of users. If they end up having to work with a proxy server that does not support tunnelling on non-443 ports, and also having to connect to a server on 80 port or others with the proxy, there would be no way for them to work around it.

Activity

  1. mcollina commented on Mar 8, 2025

    @mcollina
    SponsorMember

    I agree we should follow what curl is doing, but... I'm not sure how impactful this is, as we shouldn't really encourage anyone to pass through a proxy with plain HTTP.

    Anyhow, yes, this looks like a bug.

    This behavior is documented in https://everything.curl.dev/usingcurl/proxies/http.html#https-with-http-proxy.

  2. joyeecheung commented on Mar 8, 2025

    @joyeecheung
    MemberAuthor

    I'm not sure how impactful this is, as we shouldn't really encourage anyone to pass through a proxy with plain HTTP.

    Agreed though I think from user's perspective, how the proxy or the endpoint works isn't usually in their control, so this might come up at some point for people who have to deal with this set up..the adoption of HTTPS vary greatly depending on which part of the world you are in (e.g. see https://www.ndss-symposium.org/wp-content/uploads/madweb25-1.pdf).

    Anyway, I don't think it's urgent, just that it would be safer if we are aligned with curl eventually.

  3. caitp commented on Apr 10, 2025

    @caitp
    Contributor

    Curl provides the --proxytunnel option to allow opting into the always-tunnel behaviour even for unencrypted communication via an http proxy -- however I believe the tunnelling is required if using an https destination via an HTTP proxy, if I'm not mistaken -- which would be kind of the same thing here.

    You could imagine situations where htp->http not sending a CONNECT could break some naive state management in some (proxy) servers, though, so maybe instead of opting into the tunneling for http via http proxy, we'd want to allow explicitly opting out it with an option. Just an idea

  4. added 5 commits that reference this issue on Apr 18, 2025
    05f7ff8
    84913f3
    e5d7104
    ee6e165
    eab9234
  5. added 3 commits that reference this issue on May 12, 2025
    abbaba6
    b90ae24
    76d658d
  6. added a commit that references this issue on May 20, 2025
    95fd9d3
  7. joyeecheung commented on Jun 27, 2025

    @joyeecheung
    MemberAuthor

    Reopening it because of #4180 (comment)

  8. RafaelGSS commented on Aug 20, 2025

    @RafaelGSS
    Member

    Just saw this @joyeecheung. I'm the one who implemented the initial version of ProxyAgent and the one who changed the behaviour to CONNECT. The reason was due to GHSA-pgw7-wx7w-2w33 (in details https://blog.rafaelgss.dev/http-proxy-tunneling)

  9. joyeecheung commented on Aug 20, 2025

    @joyeecheung
    MemberAuthor

    The CVE seems to address a different problem, when the traffic is HTTPS? (When the traffic is HTTPS, it should always be tunnelled, but this issue is not about that). The reported difference is about pure HTTP traffic over HTTP proxy, which is already insecure, so not tunnelling it isn't a vulnerability but done for compatibility with actual proxies/traffic in the wild (therefore it's not a vulnerability for curl either). If the target server only provides HTTP endpoints and the user can only use HTTP proxies, and both are out of their control, the client forcing CONNECT only (incompatible with RFC 7230) means that they cannot use the client whatsoever, and their only choice is to use a curl-compatible client instead. The tunnel also doesn't enhance security, as the traffic is already plain text HTTP and there's no certificate to verify.

  10. added a commit that references this issue on Aug 21, 2025
    4beaafd
  11. added a commit that references this issue on Aug 21, 2025
    4a06ffe
  12. added a commit that references this issue on Feb 14, 2026
    2bdb19c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions