From 8dc084af5f2c23d3e97cdf955f525b2f84aa4206 Mon Sep 17 00:00:00 2001 From: robobun Date: Tue, 9 Dec 2025 12:31:45 -0800 Subject: [PATCH] fix(fetch): ignore proxy object without url property (#25414) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - When a URL object is passed as the proxy option, or when a proxy object lacks a "url" property, ignore it instead of throwing an error - This fixes a regression introduced in 1.3.4 where libraries like taze that pass URL objects as proxy values would fail ## Test plan - Added test: "proxy as URL object should be ignored (no url property)" - passes a URL object directly as proxy - Updated test: "proxy object without url is ignored (regression #25413)" - proxy object with headers but no url - Updated test: "proxy object with null url is ignored (regression #25413)" - proxy object where url is null - All 29 proxy tests pass Fixes #25413 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Bot Co-authored-by: Claude Co-authored-by: Jarred Sumner --- src/bun.js/webcore/fetch.zig | 80 +++++++++++++++++----------------- test/js/bun/http/proxy.test.ts | 63 +++++++++++++++++--------- 2 files changed, 82 insertions(+), 61 deletions(-) diff --git a/src/bun.js/webcore/fetch.zig b/src/bun.js/webcore/fetch.zig index 82fedebeaf..1d0a744b45 100644 --- a/src/bun.js/webcore/fetch.zig +++ b/src/bun.js/webcore/fetch.zig @@ -667,51 +667,51 @@ pub fn Bun__fetch_( break :extract_proxy buffer; } // Handle object format: proxy: { url: "http://proxy.example.com:8080", headers?: Headers } + // If the proxy object doesn't have a 'url' property, ignore it. + // This handles cases like passing a URL object directly as proxy (which has 'href' not 'url'). if (proxy_arg.isObject()) { // Get the URL from the proxy object - const proxy_url_arg = try proxy_arg.get(globalThis, "url"); - if (proxy_url_arg == null or proxy_url_arg.?.isUndefinedOrNull()) { - const err = ctx.toTypeError(.INVALID_ARG_VALUE, "fetch() proxy object requires a 'url' property", .{}); - is_error = true; - return JSPromise.dangerouslyCreateRejectedPromiseValueWithoutNotifyingVM(globalThis, err); - } - if (proxy_url_arg.?.isString() and try proxy_url_arg.?.getLength(ctx) > 0) { - var href = try jsc.URL.hrefFromJS(proxy_url_arg.?, globalThis); - if (href.tag == .Dead) { - const err = ctx.toTypeError(.INVALID_ARG_VALUE, "fetch() proxy URL is invalid", .{}); - is_error = true; - return JSPromise.dangerouslyCreateRejectedPromiseValueWithoutNotifyingVM(globalThis, err); - } - defer href.deref(); - const buffer = try std.fmt.allocPrint(allocator, "{s}{f}", .{ url_proxy_buffer, href }); - url = ZigURL.parse(buffer[0..url.href.len]); - if (url.isFile()) { - url_type = URLType.file; - } else if (url.isBlob()) { - url_type = URLType.blob; - } - - proxy = ZigURL.parse(buffer[url.href.len..]); - allocator.free(url_proxy_buffer); - url_proxy_buffer = buffer; - - // Get the headers from the proxy object (optional) - if (try proxy_arg.get(globalThis, "headers")) |headers_value| { - if (!headers_value.isUndefinedOrNull()) { - if (headers_value.as(FetchHeaders)) |fetch_hdrs| { - proxy_headers = Headers.from(fetch_hdrs, allocator, .{}) catch |err| bun.handleOom(err); - } else if (try FetchHeaders.createFromJS(ctx, headers_value)) |fetch_hdrs| { - defer fetch_hdrs.deref(); - proxy_headers = Headers.from(fetch_hdrs, allocator, .{}) catch |err| bun.handleOom(err); + if (try proxy_arg.get(globalThis, "url")) |proxy_url_arg| { + if (!proxy_url_arg.isUndefinedOrNull()) { + if (proxy_url_arg.isString() and try proxy_url_arg.getLength(ctx) > 0) { + var href = try jsc.URL.hrefFromJS(proxy_url_arg, globalThis); + if (href.tag == .Dead) { + const err = ctx.toTypeError(.INVALID_ARG_VALUE, "fetch() proxy URL is invalid", .{}); + is_error = true; + return JSPromise.dangerouslyCreateRejectedPromiseValueWithoutNotifyingVM(globalThis, err); } + defer href.deref(); + const buffer = try std.fmt.allocPrint(allocator, "{s}{f}", .{ url_proxy_buffer, href }); + url = ZigURL.parse(buffer[0..url.href.len]); + if (url.isFile()) { + url_type = URLType.file; + } else if (url.isBlob()) { + url_type = URLType.blob; + } + + proxy = ZigURL.parse(buffer[url.href.len..]); + allocator.free(url_proxy_buffer); + url_proxy_buffer = buffer; + + // Get the headers from the proxy object (optional) + if (try proxy_arg.get(globalThis, "headers")) |headers_value| { + if (!headers_value.isUndefinedOrNull()) { + if (headers_value.as(FetchHeaders)) |fetch_hdrs| { + proxy_headers = Headers.from(fetch_hdrs, allocator, .{}) catch |err| bun.handleOom(err); + } else if (try FetchHeaders.createFromJS(ctx, headers_value)) |fetch_hdrs| { + defer fetch_hdrs.deref(); + proxy_headers = Headers.from(fetch_hdrs, allocator, .{}) catch |err| bun.handleOom(err); + } + } + } + + break :extract_proxy url_proxy_buffer; + } else { + const err = ctx.toTypeError(.INVALID_ARG_VALUE, "fetch() proxy.url must be a non-empty string", .{}); + is_error = true; + return JSPromise.dangerouslyCreateRejectedPromiseValueWithoutNotifyingVM(globalThis, err); } } - - break :extract_proxy url_proxy_buffer; - } else { - const err = ctx.toTypeError(.INVALID_ARG_VALUE, "fetch() proxy.url must be a non-empty string", .{}); - is_error = true; - return JSPromise.dangerouslyCreateRejectedPromiseValueWithoutNotifyingVM(globalThis, err); } } } diff --git a/test/js/bun/http/proxy.test.ts b/test/js/bun/http/proxy.test.ts index 703c189085..dc5c5ec3da 100644 --- a/test/js/bun/http/proxy.test.ts +++ b/test/js/bun/http/proxy.test.ts @@ -500,29 +500,32 @@ describe("proxy object format with headers", () => { } }); - test("proxy object without url throws error", async () => { - await expect( - fetch(httpServer.url, { - method: "GET", - proxy: { - headers: { "X-Test": "value" }, - } as any, - keepalive: false, - }), - ).rejects.toThrow("fetch() proxy object requires a 'url' property"); + test("proxy object without url is ignored (regression #25413)", async () => { + // When proxy object doesn't have a 'url' property, it should be ignored + // This ensures compatibility with libraries that pass URL objects as proxy + const response = await fetch(httpServer.url, { + method: "GET", + proxy: { + headers: { "X-Test": "value" }, + } as any, + keepalive: false, + }); + expect(response.ok).toBe(true); + expect(response.status).toBe(200); }); - test("proxy object with null url throws error", async () => { - await expect( - fetch(httpServer.url, { - method: "GET", - proxy: { - url: null, - headers: { "X-Test": "value" }, - } as any, - keepalive: false, - }), - ).rejects.toThrow("fetch() proxy object requires a 'url' property"); + test("proxy object with null url is ignored (regression #25413)", async () => { + // When proxy.url is null, the proxy object should be ignored + const response = await fetch(httpServer.url, { + method: "GET", + proxy: { + url: null, + headers: { "X-Test": "value" }, + } as any, + keepalive: false, + }); + expect(response.ok).toBe(true); + expect(response.status).toBe(200); }); test("proxy object with empty string url throws error", async () => { @@ -699,4 +702,22 @@ describe("proxy object format with headers", () => { await once(proxyServerWithCapture, "close"); } }); + + test("proxy as URL object should be ignored (no url property)", async () => { + // This tests the regression from #25413 + // When a URL object is passed as proxy, it should be ignored (no error) + // because URL objects don't have a "url" property - they have "href" + const proxyUrl = new URL(httpProxyServer.url); + + // Passing a URL object as proxy should NOT throw an error + // It should just be ignored since there's no "url" string property + const response = await fetch(httpServer.url, { + method: "GET", + proxy: proxyUrl as any, + keepalive: false, + }); + // The request should succeed (without proxy, since URL object is ignored) + expect(response.ok).toBe(true); + expect(response.status).toBe(200); + }); });