logo bannerlogo banner

Battling Proxies

last edited: 2025-11-24

i wrote this in november 2025 as a test markdown-filled post for the blog, and i decided to release it for any interested parties! it's a bit technical, but it goes into the process of how i made proxies work in all 3 desktop js runtimes (node, deno, bun) without using any runtime-specific code. if that sounds perchance interesting to you, read on!

proxies are a way to funnel traffic through another network. that much is simple. however, proxies can be a nightmare to deal with when trying to make them compatible in Node, Deno, AND Bun.

in Node, this is a typical socks5 in a fetch request:

import { SocksProxyAgent } from 'socks-proxy-agent';

const agent = new SocksProxyAgent('socks5h://username:password@host:port');
const res = await fetch('https://example.com', { agent });

and in Deno...

const dispatcher = Deno.createHttpClient({
    proxy: {
        url: 'socks5h://host:port',
        basicAuth: { username: 'username', password: 'password' }
    }
});

const res = await fetch('https://example.com', { dispatcher });

and in Bun...it's not yet supported (https proxies are supported in fetch but not much else). i've been hammering them in the issues for a while now, but nothing yet. but, i love bun, and i NEEDED it to work.

the workaround

to understand the workaround, you first need to understand what i call "node internals". these are the node:* modules you interact with. if you have no idea what these are, any nodejs dev has probably used them before! fs is a "node internal". it's a package that comes with nodejs that doesn't need to be npm installed.

fs can also be imported as node:fs to make it clear that it's a node API, and it's something built into nodejs, which is why i call them "node internals".

anyway, all 3 of these runtimes (node, deno, bun) support almost all node internals. so, the question was: how do i use specific node internals to make a proxy solution work in all 3 runtimes?

answer:

  • node:dns
  • node:net
  • node:tls
  • node:zlib

in short, this combination of internals allows me to "rewrite protocols" in a way that works in all 3 runtimes. so what is protocol rewriting?

protocol rewriting

protocol rewriting is the process of taking a protocol (like https/2 or websockets) and rewriting it. https isn't just a name for secure websites, it's a protocol. protocols are a set of rules that dictate certain ways traffic is passed over a webserver.

https isn't a special type of request. it's actually just...text. all of the information in an https request is just text. the same goes for http/2 and ws (websockets). they're all just text.

this is what an https request to https://yolkbot.xyz looks like with basic headers:

GET https://yolkbot.xyz:443 HTTP/1.1
Host: yolkbot.xyz
Accept: */*
User-Agent: MyUserAgent/1.0

and a response might look like this:

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 1256

<html>
...
</html>

this is obviously VERY simplified, but you get the idea. it's all just text.

the purpose of fetch is to avoid making us send all of this text. for example, the above request using js fetch would be:

fetch('https://yolkbot.xyz', {
    headers: {
        'User-Agent': 'MyUserAgent/1.0'
    }
}).then(res => res.text()).then(body => console.log(body));

which is obviously very simple compared to...the raw text. this takes us back to the runtime issue. since fetch and WebSocket don't SUPPORT proxy options in all runtimes, we can't use them - at all. so, we have to rewrite the protocols ourselves.

under the hood, yolkbot doesn't use fetch normally. it uses node:tls and node:net to send the raw text and then parse it back. this is called "protocol rewriting" the same is true with websockets. we use tls.connect to connect to the server, then we send the raw websocket text information over that connection.

i ALSO had to do one more thing: make proxies work. since i couldn't use typical proxy implementations, guess what other protocols i had to rewrite? that's right: socks5(h)!

like https, socks5 is a protocol. it needs to have its own bytes (not quite text in the event of socks5), so i had to rewrite that protocol as well. luckily, socks5 is a LOT simpler than https, so it wasn't too bad.

socks5h is different than socks5 in that it does hostname resolution on the proxy server, rather than the client. this is used by most proxy providers, as it allows them to block domains from being loaded on proxies. my old provider, smartproxy, used this to block access to websites like Disney+ who liked to sue. luckily, my new provider supplies me with socks5 and does not block any domains, so i do not have to use socks5h.

because https was being rewritten, socks5 did too. i had to rewrite socks5's protocol to integrate with my https one and then add socks5h. i also had to test it to ensure it worked without a proxy, with a socks5 proxy, and with a socks5h proxy in all 3 runtimes. it was a LOT of work, but it paid off.

conclusion

so, that's how i battled proxies in desktop js runtimes. it was a lot of work, but i'm happy with the result. yolkbot now supports proxies in all 3 major desktop js runtimes: node, deno, and bun. yay!