From 6066c60581417d048611aa531ec583d8d6e3a83a Mon Sep 17 00:00:00 2001 From: Jamison Dance Date: Mon, 23 Mar 2026 13:38:08 -0600 Subject: [PATCH] use resolveForProxy in click handler for JS-created links the click handler only caught links starting with /browse/ or http(s)://. relative links added by JavaScript after the HTMLRewriter ran (like /page or relative.html) fell through and navigated to the proxy origin, causing 404s. now the handler uses resolveForProxy() for all links, which resolves relative URLs against the target origin. also adds mailto: and tel: to the resolveForProxy skip list, and handles // protocol-relative URLs. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/rewriter.test.ts | 11 +++++++++++ src/uppercase-script.ts | 24 ++++++++++-------------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/rewriter.test.ts b/src/rewriter.test.ts index 0ed5b62..ced5653 100644 --- a/src/rewriter.test.ts +++ b/src/rewriter.test.ts @@ -139,6 +139,17 @@ describe("HTMLRewriter integration", () => { expect(allProxied).toBe(true); }); + it("click handler uses resolveForProxy for link navigation", async () => { + const resp = await worker.fetch("/browse/https://httpbin.org/html"); + if (resp.status !== 200) return; + const html = await resp.text(); + // the click handler should use resolveForProxy instead of only matching http:// urls + expect(html).toContain("resolveForProxy(href)"); + // should handle mailto: and tel: links without proxying + expect(html).toContain("mailto:"); + expect(html).toContain("tel:"); + }); + it("forwards POST body and content-type header", async () => { const resp = await worker.fetch("/browse/https://httpbin.org/post", { method: "POST", diff --git a/src/uppercase-script.ts b/src/uppercase-script.ts index 855d780..2cda4a6 100644 --- a/src/uppercase-script.ts +++ b/src/uppercase-script.ts @@ -96,32 +96,28 @@ export const uppercaseScript = ` if (!link) return; var href = link.getAttribute('href'); if (!href) return; - // let proxy-rewritten links work naturally in the iframe + // already proxied — just tell parent the real URL if (href.startsWith(PROXY_PREFIX)) { - // extract the real URL and tell the parent to update the URL bar var realUrl = href.slice(PROXY_PREFIX.length); - try { - window.top.postMessage({ type: 'navigate', url: realUrl }, '*'); - } catch(ex) {} + try { window.top.postMessage({ type: 'navigate', url: realUrl }, '*'); } catch(ex) {} return; } - // absolute external link not yet rewritten - if (href.startsWith('http://') || href.startsWith('https://')) { + // resolve via the same logic as fetch/XHR + var resolved = resolveForProxy(href); + if (resolved !== href) { e.preventDefault(); - var proxied = PROXY_PREFIX + href; - try { - window.top.postMessage({ type: 'navigate', url: href }, '*'); - } catch(ex) {} - window.location.href = proxied; + var navigateUrl = resolved.startsWith(PROXY_PREFIX) ? resolved.slice(PROXY_PREFIX.length) : href; + try { window.top.postMessage({ type: 'navigate', url: navigateUrl }, '*'); } catch(ex) {} + window.location.href = resolved; } }, 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('#')) { + if (!url || url.startsWith(PROXY_PREFIX) || url.startsWith('data:') || url.startsWith('blob:') || url.startsWith('javascript:') || url.startsWith('#') || url.startsWith('mailto:') || url.startsWith('tel:')) { return url; } - if (url.startsWith('http://') || url.startsWith('https://')) { + if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('//')) { return PROXY_PREFIX + url; } // relative URL — resolve against the target origin