Skip to content

Conversation

@Corvince
Copy link

@Corvince Corvince commented Oct 13, 2025

This is a proof of concept to explore HTTP/2 proxy support options. The ultimate goal is enabling full HTTP/2 proxy support, which presents a significant challenge: we currently proxy through HTTP/HTTPS requests, but can't detect HTTP/2 support beforehand. This would require attempting HTTP/2 first (using a different API), then falling back to regular requests - adding considerable complexity.

I explored using the Undici client instead, which offers:

  • Unified API for HTTP/1.1, HTTPS, and HTTP/2
  • Built-in features (connection pooling, redirects, etc.)
  • Performance improvements
  • Automatic protocol negotiation

To my surprise the code already passes most of the http and websocket tests (but struggling on lib tests).

This approach has significant API compatibility issues:

  1. No custom agent support - HTTP/HTTPS agents are no longer used, breaking existing customizations.

  2. Event model changes - The proxyReq event no longer exists, though similar functionality could be provided via interceptors. This is the most critical issue since many integrations depend on these events.

The latter point is where this approach falls together, because lots of things are built around these events. But the same would be somewhat true if we use a http2 client request, there is no API compatible way to handle both http2 requests.

So I am somewhat unsure how to proceed from here. First of all as I understand it http-proxy-3 should be a drop-in replacement for http-proxy, so any API changes should probably be forbidden at this point (especially given the rather low adaptation level currently). This leaves some option to consider

  1. Just leave it as it is (supporting only incoming http2 requests), which probably isn't super future-proof
  2. Create a new API for http2 requests (which then could also be used for http1/http1.1 used)
  3. Try to recreate and/or emulate the proxyReq/proxyRes objects for http2 requests, which probably only works for simple things (e.g. returning an object with a setHeader method).

So I think the path forward depends on the general path forward and current use cases for this library, which I cannot evaluate.

@williamstein
Copy link
Contributor

It seems to me that the best option is "Create a new API for http2 requests (which then could also be used for http1/http1.1 used)". This keeps things backward compatible -- which is a hard requirement for this project -- but still provides a path for http2 support.

@Corvince
Copy link
Author

Probably makes sense. I started to go in this direction by adding "clientOptions" and "requestOptions", which are undici specific. If either of those are set, the new code path will trigger. The next step will be assessing the compatibility of the existing options with this new code path. Then I will be able to figure out when and how to warn for incompatible options . For example, setting dispatchOptions and a custom http agent will never be compatible, while followRedirects can, but simply has not been implemented so far.

@Corvince
Copy link
Author

Staring at this stuff for a few more hours I realized that most of the options stuff happens outside of the "stream" function anyway. With the current modifications almost all tests pass, the exceptions being only

  1. Tests depending on proxyReq event (1 direct tests one middleware test)
  2. Tests depending on proxyRes event (2 tests)
  3. A test that checks CamelCase of some headers (they are always lowercased with undici)
  4. A test that checks if proxyReq is skipped when an "except" header is present. Except headers are not supported by undici.

So very promising so far. My next steps would be documentation on the new available options and their implications. And to provide some callback-based alternatives to proxyReq and proxyRes to call before sending the request and after receiving the response.

@Corvince Corvince marked this pull request as ready for review October 23, 2025 19:22
@Corvince
Copy link
Author

I have converted this into a normal pull request, adding a secondary stream path using undici. This code path is triggered by setting the undici option to a truthy value and provides the following options. Its main advantages are full http2 support and better performance.

  1. agentOptions that are passed to the creation of the undici Agent that triggeres the requests. This Agent is reused across requests and should thus be quite a bit faster. Also defaults to enabling h2 requests.
  2. requestOptions that specify additional request options
  3. onBeforeRequest and onAfterResponse. These are alternatives to the "proxyReq" and "proxyRes" events of the http/https code paths.

I updated the documentation to describe these new features - and marked undici code path as experimental.

The undici code path can also be triggered by setting a ´FORCE_UNDICI_PATH´ environmental variable - this is mainly used to run the tests also for the undici code path - only 4 tests are skipped then.

The main point I am hessitant about is the specific undici naming. At first glance I think it might be too implementation specific - something that made http2 support difficult in the first place, because the legacy code is dependent on nodes http/https clients. And users of http-proxy-3 should not need to know that we are using undici. But then again this approach does use undici and the setable agent- and requestoptions are undici specific. And this way users have full control by simply wrapping those.

@williamstein
Copy link
Contributor

I haven't studied the PR enough yet, but I personally like your approach of being explicit about using undici, and also that the http2 functionality is behind that flag (or env variable). I really like the approach you've taken.

I have a question about install size, which just occurred to me. Right now the https://www.npmjs.com/package/http-proxy-3 package is 87 KB, and I think there are some users that really care about the install size. With undici, I guess the install size will increase to at least 2MB, since the install size of undici is 1.47 MB. Have you thought about how to address this?

  • I could publish two different packages (but from the same repo, and mention each in the readme)
  • something else?

@Corvince
Copy link
Author

Thanks for your encouraging words!

Regarding size, that is a tough one and not something I was aware of. But during the night I thought of something else that might also help here. I am still not sure if going undici really is the best way forward. Alternatively I could switch the code to use fetch instead, which is a much more standardized way for, well, fetching data. That means instead of undiciOptions we would have fetchOptions and have a onBeforeRequest with a web-standard Request object and an onAfterResponse with a web standard Response object.
The biggest downside of this would be lacking (default) http2 support, which was the primary point of this whole PR 🤣

But it could also be made to support passing a custom fetch method. So something like this in userland should work

import {fetch, Agent, setGlobalDispatcher} from "undici" 

function myFetch (url, opts) {
  opts ||= {};
  opts.dispatcher = new Agent({allowH2: true});
  return fetch(url, opts);
}

const proxy = createProxyServer({fetch: myFetch})

// Alternatively (but this affects all of fetch)
setGlobalDispatcher(new Agent({allowH2: true})

While a bit cumbersome, this should be more future proof and http2 support in fetch could also be the default in future node.js versions. More importantly this will be independent of the javascript runtime, so for example deno's fetch already supports http2 requests. I just tried a bit testing with bun and bun and undici do not seem to be completely compatible yet.

@williamstein
Copy link
Contributor

Yes, I really like the idea of passing something imported from undici (or whatever) in, given the role of http-proxy-3 as a long-term backward compat not too big library.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants