Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions docs/guides/http/proxy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ sidebarTitle: Proxy HTTP requests using fetch()
mode: center
---

In Bun, `fetch` supports sending requests through an HTTP or HTTPS proxy. This is useful on corporate networks or when you need to ensure a request is sent through a specific IP address.
In Bun, `fetch` supports sending requests through an HTTP, HTTPS, or SOCKS proxy. This is useful on corporate networks or when you need to ensure a request is sent through a specific IP address.

```ts proxy.ts icon="/icons/typescript.svg"
await fetch("https://example.com", {
Expand All @@ -15,7 +15,17 @@ await fetch("https://example.com", {

---

The `proxy` option can be a URL string or an object with `url` and optional `headers`. The URL can include the username and password if the proxy requires authentication. It can be `http://` or `https://`.
The `proxy` option can be a URL string or an object with `url` and optional `headers`. The URL can include the username and password if the proxy requires authentication. It can be `http://`, `https://`, `socks5://`, or `socks5h://`.

```ts socks-proxy.ts icon="/icons/typescript.svg"
await fetch("https://example.com", {
proxy: "socks5h://username:password@127.0.0.1:1080",
});
```

For SOCKS proxies, credentials are sent using SOCKS username/password authentication. With `socks5://`, Bun resolves the target hostname locally and sends the resolved IP address to the proxy. With `socks5h://`, Bun sends the hostname to the proxy and lets the proxy resolve DNS.

Custom `proxy.headers` only apply to HTTP and HTTPS proxies.

---

Expand All @@ -35,7 +45,7 @@ await fetch("https://example.com", {
});
```

The `headers` property accepts a plain object or a `Headers` instance. These headers are sent directly to the proxy server in `CONNECT` requests (for HTTPS targets) or in the proxy request (for HTTP targets).
The `headers` property accepts a plain object or a `Headers` instance. These headers are sent directly to HTTP and HTTPS proxy servers in `CONNECT` requests (for HTTPS targets) or in the proxy request (for HTTP targets).

If you provide a `Proxy-Authorization` header, it will override any credentials specified in the proxy URL.

Expand All @@ -48,3 +58,7 @@ You can also set the `$HTTP_PROXY` or `$HTTPS_PROXY` environment variable to the
```sh terminal icon="terminal"
HTTPS_PROXY=https://username:password@proxy.example.com:8080 bun run index.ts
```

```sh terminal icon="terminal"
HTTPS_PROXY=socks5h://username:password@127.0.0.1:1080 bun run index.ts
```
11 changes: 10 additions & 1 deletion docs/runtime/networking/fetch.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ const response = await fetch("http://example.com", {
});
```

The proxy URL can use `http://`, `https://`, `socks5://`, or `socks5h://`.
SOCKS proxies support username/password credentials in the proxy URL. With `socks5://`, Bun resolves the target hostname locally and sends the resolved IP address to the proxy. With `socks5h://`, Bun sends the hostname to the proxy and lets the proxy resolve DNS:

```ts
const response = await fetch("https://example.com", {
proxy: "socks5h://username:password@127.0.0.1:1080",
});
```

You can also use an object format to send custom headers to the proxy server:

```ts
Expand All @@ -73,7 +82,7 @@ const response = await fetch("http://example.com", {
});
```

The `headers` are sent directly to the proxy in `CONNECT` requests (for HTTPS targets) or in the proxy request (for HTTP targets). If you provide a `Proxy-Authorization` header, it overrides any credentials in the proxy URL.
The `headers` are sent directly to HTTP and HTTPS proxies in `CONNECT` requests (for HTTPS targets) or in the proxy request (for HTTP targets). If you provide a `Proxy-Authorization` header, it overrides any credentials in the proxy URL. Custom proxy headers do not apply to SOCKS proxies.

### Custom headers

Expand Down
9 changes: 7 additions & 2 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4219,9 +4219,14 @@ declare module "bun" {

type WebSocketOptionsProxy = {
/**
* HTTP proxy to use for the WebSocket connection.
* Proxy to use for the WebSocket connection.
*
* Can be a string URL or an object with `url` and optional `headers`.
* Supported proxy URL schemes are `http:`, `https:`, `socks5:`, and
* `socks5h:`. SOCKS proxies support username/password credentials in the
* proxy URL. `socks5:` resolves target hostnames locally; `socks5h:`
* lets the proxy resolve target hostnames. `proxy.headers` only applies
* to HTTP(S) proxies.
*
* @example
* ```ts
Expand Down Expand Up @@ -4250,7 +4255,7 @@ declare module "bun" {
| string
| {
/**
* The proxy URL (http:// or https://)
* The proxy URL (`http://`, `https://`, `socks5://`, or `socks5h://`)
*/
url: string;
/**
Expand Down
4 changes: 4 additions & 0 deletions packages/bun-types/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1937,6 +1937,10 @@ interface BunFetchRequestInit extends RequestInit {
* This is a custom property that is not part of the Fetch API specification.
*
* Can be a string URL or an object with `url` and optional `headers`.
* Supported proxy URL schemes are `http:`, `https:`, `socks5:`, and `socks5h:`.
* SOCKS proxies support username/password credentials in the proxy URL.
* `socks5:` resolves target hostnames locally; `socks5h:` lets the proxy
* resolve target hostnames. `proxy.headers` only applies to HTTP(S) proxies.
*
* @example
* ```js
Expand Down
13 changes: 9 additions & 4 deletions src/http/AsyncHTTP.zig
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ pub fn init(
.result_callback = callback,
.http_proxy = options.http_proxy,
.signals = options.signals orelse .{},
.async_http_id = if (options.signals != null and options.signals.?.aborted != null) bun.http.async_http_id_monotonic.fetchAdd(1, .monotonic) else 0,
.async_http_id = if (options.signals != null and options.signals.?.aborted != null) bun.http.nextAsyncHTTPID() else 0,
};

this.client = .{
Expand Down Expand Up @@ -213,7 +213,9 @@ pub fn init(
}

if (options.http_proxy) |proxy| {
if (proxy.username.len > 0) {
const is_socks = strings.eqlComptime(proxy.protocol, "socks5") or strings.eqlComptime(proxy.protocol, "socks5h");
this.client.flags.disable_keepalive = this.client.flags.disable_keepalive or this.url.isHTTPS() or is_socks;
if (!is_socks and proxy.username.len > 0) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// Use stack fallback allocator - stack for small credentials, heap for large ones
var username_sfb = std.heap.stackFallback(4096, allocator);
const username_alloc = username_sfb.get();
Expand Down Expand Up @@ -283,14 +285,16 @@ pub fn initSync(
}

fn reset(this: *AsyncHTTP) !void {
const disable_keepalive = this.client.flags.disable_keepalive;
const aborted = this.client.aborted;
this.client = try HTTPClient.init(this.allocator, this.method, this.client.url, this.client.header_entries, this.client.header_buf, aborted);
this.client.http_proxy = this.http_proxy;

if (this.http_proxy) |proxy| {
const is_socks = strings.eqlComptime(proxy.protocol, "socks5") or strings.eqlComptime(proxy.protocol, "socks5h");
//TODO: need to understand how is possible to reuse Proxy with TSL, so disable keepalive if url is HTTPS
this.client.flags.disable_keepalive = this.url.isHTTPS();
if (proxy.username.len > 0) {
this.client.flags.disable_keepalive = disable_keepalive or this.url.isHTTPS() or is_socks;
if (!is_socks and proxy.username.len > 0) {
// Use stack fallback allocator - stack for small credentials, heap for large ones
var username_sfb = std.heap.stackFallback(4096, this.allocator);
const username_alloc = username_sfb.get();
Expand Down Expand Up @@ -475,6 +479,7 @@ const MutableString = bun.MutableString;
const assert = bun.assert;
const jsc = bun.jsc;
const picohttp = bun.picohttp;
const strings = bun.strings;
const Channel = bun.threading.Channel;
const SSLConfig = bun.api.server.ServerConfig.SSLConfig;

Expand Down
22 changes: 17 additions & 5 deletions src/http/HTTPThread.zig
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,10 @@ pub fn connect(this: *@This(), client: *HTTPClient, comptime is_ssl: bool) !?New
client.setCustomSslCtx(entry.ctx);
// Keepalive is now supported for custom SSL contexts
if (client.http_proxy) |url| {
return try entry.ctx.connect(client, url.hostname, url.getPortAuto());
if (!(url.protocol.len == 0 or strings.eqlComptime(url.protocol, "https") or strings.eqlComptime(url.protocol, "http") or strings.eqlComptime(url.protocol, "socks5") or strings.eqlComptime(url.protocol, "socks5h"))) {
return error.UnsupportedProxyProtocol;
}
return try entry.ctx.connect(client, url.hostname, proxyPort(url));
} else {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return try entry.ctx.connect(client, client.url.hostname, client.url.getPortAuto());
}
Expand Down Expand Up @@ -312,8 +315,8 @@ pub fn connect(this: *@This(), client: *HTTPClient, comptime is_ssl: bool) !?New
client.setCustomSslCtx(custom_context);
// Keepalive is now supported for custom SSL contexts
if (client.http_proxy) |url| {
if (url.protocol.len == 0 or strings.eqlComptime(url.protocol, "https") or strings.eqlComptime(url.protocol, "http")) {
return try custom_context.connect(client, url.hostname, url.getPortAuto());
if (url.protocol.len == 0 or strings.eqlComptime(url.protocol, "https") or strings.eqlComptime(url.protocol, "http") or strings.eqlComptime(url.protocol, "socks5") or strings.eqlComptime(url.protocol, "socks5h")) {
return try custom_context.connect(client, url.hostname, proxyPort(url));
}
return error.UnsupportedProxyProtocol;
}
Expand All @@ -323,8 +326,8 @@ pub fn connect(this: *@This(), client: *HTTPClient, comptime is_ssl: bool) !?New
if (client.http_proxy) |url| {
if (url.href.len > 0) {
// https://github.com/oven-sh/bun/issues/11343
if (url.protocol.len == 0 or strings.eqlComptime(url.protocol, "https") or strings.eqlComptime(url.protocol, "http")) {
return try this.context(is_ssl).connect(client, url.hostname, url.getPortAuto());
if (url.protocol.len == 0 or strings.eqlComptime(url.protocol, "https") or strings.eqlComptime(url.protocol, "http") or strings.eqlComptime(url.protocol, "socks5") or strings.eqlComptime(url.protocol, "socks5h")) {
return try this.context(is_ssl).connect(client, url.hostname, proxyPort(url));
}
return error.UnsupportedProxyProtocol;
}
Expand Down Expand Up @@ -525,6 +528,7 @@ fn drainEvents(this: *@This()) void {
this.drainQueuedWrites();
this.drainQueuedShutdowns();
bun.http.H3.PendingConnect.drainResolved();
bun.http.SocksDNSPending.drainResolved();

for (this.queued_threadlocal_proxy_derefs.items) |http| {
http.deref();
Expand Down Expand Up @@ -729,6 +733,14 @@ pub fn schedule(this: *@This(), batch: Batch) void {

pub const Queue = UnboundedQueue(AsyncHTTP, .next);

fn proxyPort(url: bun.URL) u16 {
if (strings.eqlComptime(url.protocol, "socks5") or strings.eqlComptime(url.protocol, "socks5h")) {
if (url.getPort()) |_| return url.getPortAuto();
return 1080;
}
return url.getPortAuto();
}

const log = Output.scoped(.HTTPThread, .visible);

const stringZ = [:0]const u8;
Expand Down
Loading