From 74e3c29d98641159b69741d56f63fd93b04864a6 Mon Sep 17 00:00:00 2001 From: Jamison Dance Date: Mon, 23 Mar 2026 11:07:35 -0600 Subject: [PATCH] resolve relative URLs in client-side fetch/XHR patches the fetch/XHR patches only caught absolute URLs (http:// / https://). relative URLs like /api/data or assets/chunk.js resolved against the proxy origin (localhost:8788) instead of the target site, causing API calls and dynamic script loading to 404. adds resolveForProxy() which extracts the target origin from window.location.pathname and resolves relative URLs against it before proxying. handles all edge cases: /browse/ paths, data/blob URIs, absolute URLs, and root-relative/relative paths. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/rewriter.test.ts | 8 ++++++++ src/uppercase-script.ts | 32 ++++++++++++++++++++++++++------ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/rewriter.test.ts b/src/rewriter.test.ts index 65a54f4..f379c4e 100644 --- a/src/rewriter.test.ts +++ b/src/rewriter.test.ts @@ -108,6 +108,14 @@ describe("HTMLRewriter integration", () => { expect(resp.headers.get("access-control-allow-origin")).toBe("*"); }); + it("injects resolveForProxy for relative URL handling", async () => { + const resp = await worker.fetch("/browse/https://httpbin.org/html"); + if (resp.status !== 200) return; + const html = await resp.text(); + expect(html).toContain("function resolveForProxy"); + expect(html).toContain("new URL(url, targetUrl)"); + }); + it("rewrites data-src attributes for lazy-loaded images", async () => { const resp = await worker.fetch("/browse/https://www.wired.com"); if (resp.status !== 200) return; diff --git a/src/uppercase-script.ts b/src/uppercase-script.ts index d9d7893..855d780 100644 --- a/src/uppercase-script.ts +++ b/src/uppercase-script.ts @@ -116,13 +116,33 @@ export const uppercaseScript = ` } }, true); + // resolve any URL to a proxied URL + function resolveForProxy(url) { + if (!url || url.startsWith(PROXY_PREFIX) || url.startsWith('data:') || url.startsWith('blob:') || url.startsWith('javascript:') || url.startsWith('#')) { + return url; + } + if (url.startsWith('http://') || url.startsWith('https://')) { + return PROXY_PREFIX + url; + } + // relative URL — resolve against the target origin + var path = window.location.pathname; + if (path.startsWith(PROXY_PREFIX)) { + var targetUrl = path.slice(PROXY_PREFIX.length) + window.location.search; + try { + var resolved = new URL(url, targetUrl).href; + return PROXY_PREFIX + resolved; + } catch(e) {} + } + return url; + } + // patch fetch var origFetch = window.fetch; window.fetch = function(input, init) { - if (typeof input === 'string' && (input.startsWith('http://') || input.startsWith('https://'))) { - input = PROXY_PREFIX + input; - } else if (input instanceof Request && (input.url.startsWith('http://') || input.url.startsWith('https://')) && !input.url.includes(PROXY_PREFIX)) { - input = new Request(PROXY_PREFIX + input.url, input); + if (typeof input === 'string') { + input = resolveForProxy(input); + } else if (input instanceof Request && !input.url.includes(PROXY_PREFIX)) { + input = new Request(resolveForProxy(input.url), input); } return origFetch.call(this, input, init); }; @@ -130,8 +150,8 @@ export const uppercaseScript = ` // patch XHR var origOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(method, url) { - if (typeof url === 'string' && (url.startsWith('http://') || url.startsWith('https://')) && !url.includes(PROXY_PREFIX)) { - url = PROXY_PREFIX + url; + if (typeof url === 'string') { + url = resolveForProxy(url); } return origOpen.apply(this, [method, url, ...Array.prototype.slice.call(arguments, 2)]); };