[Bug]: Preview address bar rewrites localhost URLs to WSL IP, breaking loopback-only and HTTPS servers #3938

Description

@Chrrxs

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/desktop

Steps to reproduce

  1. Run T3 Code Desktop on Windows with a WSL environment/backend selected.

  2. Inside WSL, start a server that listens only on loopback:

    python3 -m http.server 8000 --bind 127.0.0.1
  3. Confirm http://localhost:8000/ works from a normal Windows browser through WSL localhost forwarding.

  4. Open T3 Code's built-in preview browser.

  5. Enter http://localhost:8000/ or http://127.0.0.1:8000/ in the preview address bar.

  6. Repeat with an HTTPS loopback URL such as https://localhost:8000/ when using a local HTTPS development server.

Expected behavior

A URL entered in the preview address bar should retain its requested origin. In particular:

  • http://localhost:8000/ should remain http://localhost:8000/.
  • http://127.0.0.1:8000/ should remain http://127.0.0.1:8000/.
  • https://localhost:8000/ should remain https://localhost:8000/.

Environment-relative port navigation may translate hosts when explicitly requested, but manually entered direct URLs should not silently change origin.

Actual behavior

The preview address bar rewrites loopback URLs to the active WSL environment's private eth0 address before Electron loads them. On the affected machine, the current WSL address was 172.25.85.75:

http://localhost:8000/ -> http://172.25.85.75:8000/
http://127.0.0.1:8000/ -> http://172.25.85.75:8000/
https://localhost:8000/ -> https://172.25.85.75:8000/

This is a client-side URL substitution rather than an HTTP redirect: the loopback server returns 200 without a Location header, and direct navigation to localhost does not change the hostname.

A server bound only to 127.0.0.1 becomes unreachable through the substituted WSL address. HTTPS development servers can additionally fail certificate validation because a certificate for localhost does not cover the WSL IP. The origin change can also affect cookies, CORS, allowed-host checks, and OAuth callback URLs.

The behavior is deterministic in the shipped source:

  • apps/server/src/components/preview/PreviewView.tsx sends every address-bar submission through resolveDiscoveredServerUrl().
  • apps/server/src/browser/browserTargetResolver.ts turns any recognized loopback URL into an environment-port target when the environment connection host is non-loopback.
  • The environment-port resolver replaces the requested hostname with the hostname from connection.httpBaseUrl while preserving protocol, port, path, query, and hash.
  • apps/server/src/preview/PortScanner.ts normalizes both loopback-only and wildcard listeners to localhost, discarding the bind scope needed to determine whether the WSL IP is reachable.

The browser target interface already distinguishes { kind: "url" } from { kind: "environment-port" }. Address-bar submissions appear to be using the discovered/environment-port behavior even though they are direct URL requests.

Impact

Major degradation or frequent failure

Version or commit

T3 Code desktop 0.0.29-nightly.20260712.791; the same behavior was initially reproduced on 0.0.28.

Environment

Windows 10.0.26200.8655, WSL2 kernel 6.6.87.2-microsoft-standard-WSL2, Ubuntu 24.04, x86_64.

Logs or stack traces

$ hostname -I
172.25.85.75 10.0.0.4 172.19.0.1 172.17.0.1 172.18.0.1
$ curl -I http://localhost:8000/
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.12.3
# No Location header# T3 preview environment-port resolution, changing only protocol:
protocol=http -> http://172.25.85.75:8000/
protocol=https -> https://172.25.85.75:8000/

Screenshots, recordings, or supporting files

No attachment. The final URL shown in the preview address bar and the deterministic resolver behavior above capture the failure.

Workaround

Open the original localhost URL in a normal Windows browser.

For HTTP-only development, binding the server to 0.0.0.0 can make it reachable through the substituted WSL IP, but this broadens exposure and does not solve HTTPS origin/certificate problems.

A focused product fix would treat address-bar submissions as direct { kind: "url" } navigation and reserve { kind: "environment-port" } translation for discovered local-server cards or other explicitly environment-relative navigation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
       blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      
      Skip to content

      [Bug]: Preview address bar rewrites localhost URLs to WSL IP, breaking loopback-only and HTTPS servers #3938

      Description

      @Chrrxs

      Before submitting

      • I searched existing issues and did not find a duplicate.
      • I included enough detail to reproduce or investigate the problem.

      Area

      apps/desktop

      Steps to reproduce

      1. Run T3 Code Desktop on Windows with a WSL environment/backend selected.

      2. Inside WSL, start a server that listens only on loopback:

        python3 -m http.server 8000 --bind 127.0.0.1
      3. Confirm http://localhost:8000/ works from a normal Windows browser through WSL localhost forwarding.

      4. Open T3 Code's built-in preview browser.

      5. Enter http://localhost:8000/ or http://127.0.0.1:8000/ in the preview address bar.

      6. Repeat with an HTTPS loopback URL such as https://localhost:8000/ when using a local HTTPS development server.

      Expected behavior

      A URL entered in the preview address bar should retain its requested origin. In particular:

      • http://localhost:8000/ should remain http://localhost:8000/.
      • http://127.0.0.1:8000/ should remain http://127.0.0.1:8000/.
      • https://localhost:8000/ should remain https://localhost:8000/.

      Environment-relative port navigation may translate hosts when explicitly requested, but manually entered direct URLs should not silently change origin.

      Actual behavior

      The preview address bar rewrites loopback URLs to the active WSL environment's private eth0 address before Electron loads them. On the affected machine, the current WSL address was 172.25.85.75:

      http://localhost:8000/ -> http://172.25.85.75:8000/
      http://127.0.0.1:8000/ -> http://172.25.85.75:8000/
      https://localhost:8000/ -> https://172.25.85.75:8000/
      

      This is a client-side URL substitution rather than an HTTP redirect: the loopback server returns 200 without a Location header, and direct navigation to localhost does not change the hostname.

      A server bound only to 127.0.0.1 becomes unreachable through the substituted WSL address. HTTPS development servers can additionally fail certificate validation because a certificate for localhost does not cover the WSL IP. The origin change can also affect cookies, CORS, allowed-host checks, and OAuth callback URLs.

      The behavior is deterministic in the shipped source:

      • apps/server/src/components/preview/PreviewView.tsx sends every address-bar submission through resolveDiscoveredServerUrl().
      • apps/server/src/browser/browserTargetResolver.ts turns any recognized loopback URL into an environment-port target when the environment connection host is non-loopback.
      • The environment-port resolver replaces the requested hostname with the hostname from connection.httpBaseUrl while preserving protocol, port, path, query, and hash.
      • apps/server/src/preview/PortScanner.ts normalizes both loopback-only and wildcard listeners to localhost, discarding the bind scope needed to determine whether the WSL IP is reachable.

      The browser target interface already distinguishes { kind: "url" } from { kind: "environment-port" }. Address-bar submissions appear to be using the discovered/environment-port behavior even though they are direct URL requests.

      Impact

      Major degradation or frequent failure

      Version or commit

      T3 Code desktop 0.0.29-nightly.20260712.791; the same behavior was initially reproduced on 0.0.28.

      Environment

      Windows 10.0.26200.8655, WSL2 kernel 6.6.87.2-microsoft-standard-WSL2, Ubuntu 24.04, x86_64.

      Logs or stack traces

      $ hostname -I
      172.25.85.75 10.0.0.4 172.19.0.1 172.17.0.1 172.18.0.1
      $ curl -I http://localhost:8000/
      HTTP/1.0 200 OK
      Server: SimpleHTTP/0.6 Python/3.12.3
      # No Location header# T3 preview environment-port resolution, changing only protocol:
      protocol=http -> http://172.25.85.75:8000/
      protocol=https -> https://172.25.85.75:8000/

      Screenshots, recordings, or supporting files

      No attachment. The final URL shown in the preview address bar and the deterministic resolver behavior above capture the failure.

      Workaround

      Open the original localhost URL in a normal Windows browser.

      For HTTP-only development, binding the server to 0.0.0.0 can make it reachable through the substituted WSL IP, but this broadens exposure and does not solve HTTPS origin/certificate problems.

      A focused product fix would treat address-bar submissions as direct { kind: "url" } navigation and reserve { kind: "environment-port" } translation for discovered local-server cards or other explicitly environment-relative navigation.

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        No labels
        No labels

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          Skip to content

          [Bug]: Preview address bar rewrites localhost URLs to WSL IP, breaking loopback-only and HTTPS servers #3938

          Description

          @Chrrxs

          Before submitting

          • I searched existing issues and did not find a duplicate.
          • I included enough detail to reproduce or investigate the problem.

          Area

          apps/desktop

          Steps to reproduce

          1. Run T3 Code Desktop on Windows with a WSL environment/backend selected.

          2. Inside WSL, start a server that listens only on loopback:

            python3 -m http.server 8000 --bind 127.0.0.1
          3. Confirm http://localhost:8000/ works from a normal Windows browser through WSL localhost forwarding.

          4. Open T3 Code's built-in preview browser.

          5. Enter http://localhost:8000/ or http://127.0.0.1:8000/ in the preview address bar.

          6. Repeat with an HTTPS loopback URL such as https://localhost:8000/ when using a local HTTPS development server.

          Expected behavior

          A URL entered in the preview address bar should retain its requested origin. In particular:

          • http://localhost:8000/ should remain http://localhost:8000/.
          • http://127.0.0.1:8000/ should remain http://127.0.0.1:8000/.
          • https://localhost:8000/ should remain https://localhost:8000/.

          Environment-relative port navigation may translate hosts when explicitly requested, but manually entered direct URLs should not silently change origin.

          Actual behavior

          The preview address bar rewrites loopback URLs to the active WSL environment's private eth0 address before Electron loads them. On the affected machine, the current WSL address was 172.25.85.75:

          http://localhost:8000/ -> http://172.25.85.75:8000/
          http://127.0.0.1:8000/ -> http://172.25.85.75:8000/
          https://localhost:8000/ -> https://172.25.85.75:8000/
          

          This is a client-side URL substitution rather than an HTTP redirect: the loopback server returns 200 without a Location header, and direct navigation to localhost does not change the hostname.

          A server bound only to 127.0.0.1 becomes unreachable through the substituted WSL address. HTTPS development servers can additionally fail certificate validation because a certificate for localhost does not cover the WSL IP. The origin change can also affect cookies, CORS, allowed-host checks, and OAuth callback URLs.

          The behavior is deterministic in the shipped source:

          • apps/server/src/components/preview/PreviewView.tsx sends every address-bar submission through resolveDiscoveredServerUrl().
          • apps/server/src/browser/browserTargetResolver.ts turns any recognized loopback URL into an environment-port target when the environment connection host is non-loopback.
          • The environment-port resolver replaces the requested hostname with the hostname from connection.httpBaseUrl while preserving protocol, port, path, query, and hash.
          • apps/server/src/preview/PortScanner.ts normalizes both loopback-only and wildcard listeners to localhost, discarding the bind scope needed to determine whether the WSL IP is reachable.

          The browser target interface already distinguishes { kind: "url" } from { kind: "environment-port" }. Address-bar submissions appear to be using the discovered/environment-port behavior even though they are direct URL requests.

          Impact

          Major degradation or frequent failure

          Version or commit

          T3 Code desktop 0.0.29-nightly.20260712.791; the same behavior was initially reproduced on 0.0.28.

          Environment

          Windows 10.0.26200.8655, WSL2 kernel 6.6.87.2-microsoft-standard-WSL2, Ubuntu 24.04, x86_64.

          Logs or stack traces

          $ hostname -I
          172.25.85.75 10.0.0.4 172.19.0.1 172.17.0.1 172.18.0.1
          $ curl -I http://localhost:8000/
          HTTP/1.0 200 OK
          Server: SimpleHTTP/0.6 Python/3.12.3
          # No Location header# T3 preview environment-port resolution, changing only protocol:
          protocol=http -> http://172.25.85.75:8000/
          protocol=https -> https://172.25.85.75:8000/

          Screenshots, recordings, or supporting files

          No attachment. The final URL shown in the preview address bar and the deterministic resolver behavior above capture the failure.

          Workaround

          Open the original localhost URL in a normal Windows browser.

          For HTTP-only development, binding the server to 0.0.0.0 can make it reachable through the substituted WSL IP, but this broadens exposure and does not solve HTTPS origin/certificate problems.

          A focused product fix would treat address-bar submissions as direct { kind: "url" } navigation and reserve { kind: "environment-port" } translation for discovered local-server cards or other explicitly environment-relative navigation.

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            No labels
            No labels

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              [Bug]: Preview address bar rewrites localhost URLs to WSL IP, breaking loopback-only and HTTPS servers #3938

              Description

              @Chrrxs

              Before submitting

              • I searched existing issues and did not find a duplicate.
              • I included enough detail to reproduce or investigate the problem.

              Area

              apps/desktop

              Steps to reproduce

              1. Run T3 Code Desktop on Windows with a WSL environment/backend selected.

              2. Inside WSL, start a server that listens only on loopback:

                python3 -m http.server 8000 --bind 127.0.0.1
              3. Confirm http://localhost:8000/ works from a normal Windows browser through WSL localhost forwarding.

              4. Open T3 Code's built-in preview browser.

              5. Enter http://localhost:8000/ or http://127.0.0.1:8000/ in the preview address bar.

              6. Repeat with an HTTPS loopback URL such as https://localhost:8000/ when using a local HTTPS development server.

              Expected behavior

              A URL entered in the preview address bar should retain its requested origin. In particular:

              • http://localhost:8000/ should remain http://localhost:8000/.
              • http://127.0.0.1:8000/ should remain http://127.0.0.1:8000/.
              • https://localhost:8000/ should remain https://localhost:8000/.

              Environment-relative port navigation may translate hosts when explicitly requested, but manually entered direct URLs should not silently change origin.

              Actual behavior

              The preview address bar rewrites loopback URLs to the active WSL environment's private eth0 address before Electron loads them. On the affected machine, the current WSL address was 172.25.85.75:

              http://localhost:8000/ -> http://172.25.85.75:8000/
              http://127.0.0.1:8000/ -> http://172.25.85.75:8000/
              https://localhost:8000/ -> https://172.25.85.75:8000/
              

              This is a client-side URL substitution rather than an HTTP redirect: the loopback server returns 200 without a Location header, and direct navigation to localhost does not change the hostname.

              A server bound only to 127.0.0.1 becomes unreachable through the substituted WSL address. HTTPS development servers can additionally fail certificate validation because a certificate for localhost does not cover the WSL IP. The origin change can also affect cookies, CORS, allowed-host checks, and OAuth callback URLs.

              The behavior is deterministic in the shipped source:

              • apps/server/src/components/preview/PreviewView.tsx sends every address-bar submission through resolveDiscoveredServerUrl().
              • apps/server/src/browser/browserTargetResolver.ts turns any recognized loopback URL into an environment-port target when the environment connection host is non-loopback.
              • The environment-port resolver replaces the requested hostname with the hostname from connection.httpBaseUrl while preserving protocol, port, path, query, and hash.
              • apps/server/src/preview/PortScanner.ts normalizes both loopback-only and wildcard listeners to localhost, discarding the bind scope needed to determine whether the WSL IP is reachable.

              The browser target interface already distinguishes { kind: "url" } from { kind: "environment-port" }. Address-bar submissions appear to be using the discovered/environment-port behavior even though they are direct URL requests.

              Impact

              Major degradation or frequent failure

              Version or commit

              T3 Code desktop 0.0.29-nightly.20260712.791; the same behavior was initially reproduced on 0.0.28.

              Environment

              Windows 10.0.26200.8655, WSL2 kernel 6.6.87.2-microsoft-standard-WSL2, Ubuntu 24.04, x86_64.

              Logs or stack traces

              $ hostname -I
              172.25.85.75 10.0.0.4 172.19.0.1 172.17.0.1 172.18.0.1
              $ curl -I http://localhost:8000/
              HTTP/1.0 200 OK
              Server: SimpleHTTP/0.6 Python/3.12.3
              # No Location header# T3 preview environment-port resolution, changing only protocol:
              protocol=http -> http://172.25.85.75:8000/
              protocol=https -> https://172.25.85.75:8000/

              Screenshots, recordings, or supporting files

              No attachment. The final URL shown in the preview address bar and the deterministic resolver behavior above capture the failure.

              Workaround

              Open the original localhost URL in a normal Windows browser.

              For HTTP-only development, binding the server to 0.0.0.0 can make it reachable through the substituted WSL IP, but this broadens exposure and does not solve HTTPS origin/certificate problems.

              A focused product fix would treat address-bar submissions as direct { kind: "url" } navigation and reserve { kind: "environment-port" } translation for discovered local-server cards or other explicitly environment-relative navigation.

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                No labels
                No labels

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  Skip to content

                  [Bug]: Preview address bar rewrites localhost URLs to WSL IP, breaking loopback-only and HTTPS servers #3938

                  Description

                  @Chrrxs

                  Before submitting

                  • I searched existing issues and did not find a duplicate.
                  • I included enough detail to reproduce or investigate the problem.

                  Area

                  apps/desktop

                  Steps to reproduce

                  1. Run T3 Code Desktop on Windows with a WSL environment/backend selected.

                  2. Inside WSL, start a server that listens only on loopback:

                    python3 -m http.server 8000 --bind 127.0.0.1
                  3. Confirm http://localhost:8000/ works from a normal Windows browser through WSL localhost forwarding.

                  4. Open T3 Code's built-in preview browser.

                  5. Enter http://localhost:8000/ or http://127.0.0.1:8000/ in the preview address bar.

                  6. Repeat with an HTTPS loopback URL such as https://localhost:8000/ when using a local HTTPS development server.

                  Expected behavior

                  A URL entered in the preview address bar should retain its requested origin. In particular:

                  • http://localhost:8000/ should remain http://localhost:8000/.
                  • http://127.0.0.1:8000/ should remain http://127.0.0.1:8000/.
                  • https://localhost:8000/ should remain https://localhost:8000/.

                  Environment-relative port navigation may translate hosts when explicitly requested, but manually entered direct URLs should not silently change origin.

                  Actual behavior

                  The preview address bar rewrites loopback URLs to the active WSL environment's private eth0 address before Electron loads them. On the affected machine, the current WSL address was 172.25.85.75:

                  http://localhost:8000/ -> http://172.25.85.75:8000/
                  http://127.0.0.1:8000/ -> http://172.25.85.75:8000/
                  https://localhost:8000/ -> https://172.25.85.75:8000/
                  

                  This is a client-side URL substitution rather than an HTTP redirect: the loopback server returns 200 without a Location header, and direct navigation to localhost does not change the hostname.

                  A server bound only to 127.0.0.1 becomes unreachable through the substituted WSL address. HTTPS development servers can additionally fail certificate validation because a certificate for localhost does not cover the WSL IP. The origin change can also affect cookies, CORS, allowed-host checks, and OAuth callback URLs.

                  The behavior is deterministic in the shipped source:

                  • apps/server/src/components/preview/PreviewView.tsx sends every address-bar submission through resolveDiscoveredServerUrl().
                  • apps/server/src/browser/browserTargetResolver.ts turns any recognized loopback URL into an environment-port target when the environment connection host is non-loopback.
                  • The environment-port resolver replaces the requested hostname with the hostname from connection.httpBaseUrl while preserving protocol, port, path, query, and hash.
                  • apps/server/src/preview/PortScanner.ts normalizes both loopback-only and wildcard listeners to localhost, discarding the bind scope needed to determine whether the WSL IP is reachable.

                  The browser target interface already distinguishes { kind: "url" } from { kind: "environment-port" }. Address-bar submissions appear to be using the discovered/environment-port behavior even though they are direct URL requests.

                  Impact

                  Major degradation or frequent failure

                  Version or commit

                  T3 Code desktop 0.0.29-nightly.20260712.791; the same behavior was initially reproduced on 0.0.28.

                  Environment

                  Windows 10.0.26200.8655, WSL2 kernel 6.6.87.2-microsoft-standard-WSL2, Ubuntu 24.04, x86_64.

                  Logs or stack traces

                  $ hostname -I
                  172.25.85.75 10.0.0.4 172.19.0.1 172.17.0.1 172.18.0.1
                  $ curl -I http://localhost:8000/
                  HTTP/1.0 200 OK
                  Server: SimpleHTTP/0.6 Python/3.12.3
                  # No Location header# T3 preview environment-port resolution, changing only protocol:
                  protocol=http -> http://172.25.85.75:8000/
                  protocol=https -> https://172.25.85.75:8000/

                  Screenshots, recordings, or supporting files

                  No attachment. The final URL shown in the preview address bar and the deterministic resolver behavior above capture the failure.

                  Workaround

                  Open the original localhost URL in a normal Windows browser.

                  For HTTP-only development, binding the server to 0.0.0.0 can make it reachable through the substituted WSL IP, but this broadens exposure and does not solve HTTPS origin/certificate problems.

                  A focused product fix would treat address-bar submissions as direct { kind: "url" } navigation and reserve { kind: "environment-port" } translation for discovered local-server cards or other explicitly environment-relative navigation.

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Labels

                    No labels
                    No labels

                    Type

                    No type

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      Skip to content

                      [Bug]: Preview address bar rewrites localhost URLs to WSL IP, breaking loopback-only and HTTPS servers #3938

                      Description

                      @Chrrxs

                      Before submitting

                      • I searched existing issues and did not find a duplicate.
                      • I included enough detail to reproduce or investigate the problem.

                      Area

                      apps/desktop

                      Steps to reproduce

                      1. Run T3 Code Desktop on Windows with a WSL environment/backend selected.

                      2. Inside WSL, start a server that listens only on loopback:

                        python3 -m http.server 8000 --bind 127.0.0.1
                      3. Confirm http://localhost:8000/ works from a normal Windows browser through WSL localhost forwarding.

                      4. Open T3 Code's built-in preview browser.

                      5. Enter http://localhost:8000/ or http://127.0.0.1:8000/ in the preview address bar.

                      6. Repeat with an HTTPS loopback URL such as https://localhost:8000/ when using a local HTTPS development server.

                      Expected behavior

                      A URL entered in the preview address bar should retain its requested origin. In particular:

                      • http://localhost:8000/ should remain http://localhost:8000/.
                      • http://127.0.0.1:8000/ should remain http://127.0.0.1:8000/.
                      • https://localhost:8000/ should remain https://localhost:8000/.

                      Environment-relative port navigation may translate hosts when explicitly requested, but manually entered direct URLs should not silently change origin.

                      Actual behavior

                      The preview address bar rewrites loopback URLs to the active WSL environment's private eth0 address before Electron loads them. On the affected machine, the current WSL address was 172.25.85.75:

                      http://localhost:8000/ -> http://172.25.85.75:8000/
                      http://127.0.0.1:8000/ -> http://172.25.85.75:8000/
                      https://localhost:8000/ -> https://172.25.85.75:8000/
                      

                      This is a client-side URL substitution rather than an HTTP redirect: the loopback server returns 200 without a Location header, and direct navigation to localhost does not change the hostname.

                      A server bound only to 127.0.0.1 becomes unreachable through the substituted WSL address. HTTPS development servers can additionally fail certificate validation because a certificate for localhost does not cover the WSL IP. The origin change can also affect cookies, CORS, allowed-host checks, and OAuth callback URLs.

                      The behavior is deterministic in the shipped source:

                      • apps/server/src/components/preview/PreviewView.tsx sends every address-bar submission through resolveDiscoveredServerUrl().
                      • apps/server/src/browser/browserTargetResolver.ts turns any recognized loopback URL into an environment-port target when the environment connection host is non-loopback.
                      • The environment-port resolver replaces the requested hostname with the hostname from connection.httpBaseUrl while preserving protocol, port, path, query, and hash.
                      • apps/server/src/preview/PortScanner.ts normalizes both loopback-only and wildcard listeners to localhost, discarding the bind scope needed to determine whether the WSL IP is reachable.

                      The browser target interface already distinguishes { kind: "url" } from { kind: "environment-port" }. Address-bar submissions appear to be using the discovered/environment-port behavior even though they are direct URL requests.

                      Impact

                      Major degradation or frequent failure

                      Version or commit

                      T3 Code desktop 0.0.29-nightly.20260712.791; the same behavior was initially reproduced on 0.0.28.

                      Environment

                      Windows 10.0.26200.8655, WSL2 kernel 6.6.87.2-microsoft-standard-WSL2, Ubuntu 24.04, x86_64.

                      Logs or stack traces

                      $ hostname -I
                      172.25.85.75 10.0.0.4 172.19.0.1 172.17.0.1 172.18.0.1
                      $ curl -I http://localhost:8000/
                      HTTP/1.0 200 OK
                      Server: SimpleHTTP/0.6 Python/3.12.3
                      # No Location header# T3 preview environment-port resolution, changing only protocol:
                      protocol=http -> http://172.25.85.75:8000/
                      protocol=https -> https://172.25.85.75:8000/

                      Screenshots, recordings, or supporting files

                      No attachment. The final URL shown in the preview address bar and the deterministic resolver behavior above capture the failure.

                      Workaround

                      Open the original localhost URL in a normal Windows browser.

                      For HTTP-only development, binding the server to 0.0.0.0 can make it reachable through the substituted WSL IP, but this broadens exposure and does not solve HTTPS origin/certificate problems.

                      A focused product fix would treat address-bar submissions as direct { kind: "url" } navigation and reserve { kind: "environment-port" } translation for discovered local-server cards or other explicitly environment-relative navigation.

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Labels

                        No labels
                        No labels

                        Type

                        No type

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          Skip to content

                          [Bug]: Preview address bar rewrites localhost URLs to WSL IP, breaking loopback-only and HTTPS servers #3938

                          Description

                          @Chrrxs

                          Before submitting

                          • I searched existing issues and did not find a duplicate.
                          • I included enough detail to reproduce or investigate the problem.

                          Area

                          apps/desktop

                          Steps to reproduce

                          1. Run T3 Code Desktop on Windows with a WSL environment/backend selected.

                          2. Inside WSL, start a server that listens only on loopback:

                            python3 -m http.server 8000 --bind 127.0.0.1
                          3. Confirm http://localhost:8000/ works from a normal Windows browser through WSL localhost forwarding.

                          4. Open T3 Code's built-in preview browser.

                          5. Enter http://localhost:8000/ or http://127.0.0.1:8000/ in the preview address bar.

                          6. Repeat with an HTTPS loopback URL such as https://localhost:8000/ when using a local HTTPS development server.

                          Expected behavior

                          A URL entered in the preview address bar should retain its requested origin. In particular:

                          • http://localhost:8000/ should remain http://localhost:8000/.
                          • http://127.0.0.1:8000/ should remain http://127.0.0.1:8000/.
                          • https://localhost:8000/ should remain https://localhost:8000/.

                          Environment-relative port navigation may translate hosts when explicitly requested, but manually entered direct URLs should not silently change origin.

                          Actual behavior

                          The preview address bar rewrites loopback URLs to the active WSL environment's private eth0 address before Electron loads them. On the affected machine, the current WSL address was 172.25.85.75:

                          http://localhost:8000/ -> http://172.25.85.75:8000/
                          http://127.0.0.1:8000/ -> http://172.25.85.75:8000/
                          https://localhost:8000/ -> https://172.25.85.75:8000/
                          

                          This is a client-side URL substitution rather than an HTTP redirect: the loopback server returns 200 without a Location header, and direct navigation to localhost does not change the hostname.

                          A server bound only to 127.0.0.1 becomes unreachable through the substituted WSL address. HTTPS development servers can additionally fail certificate validation because a certificate for localhost does not cover the WSL IP. The origin change can also affect cookies, CORS, allowed-host checks, and OAuth callback URLs.

                          The behavior is deterministic in the shipped source:

                          • apps/server/src/components/preview/PreviewView.tsx sends every address-bar submission through resolveDiscoveredServerUrl().
                          • apps/server/src/browser/browserTargetResolver.ts turns any recognized loopback URL into an environment-port target when the environment connection host is non-loopback.
                          • The environment-port resolver replaces the requested hostname with the hostname from connection.httpBaseUrl while preserving protocol, port, path, query, and hash.
                          • apps/server/src/preview/PortScanner.ts normalizes both loopback-only and wildcard listeners to localhost, discarding the bind scope needed to determine whether the WSL IP is reachable.

                          The browser target interface already distinguishes { kind: "url" } from { kind: "environment-port" }. Address-bar submissions appear to be using the discovered/environment-port behavior even though they are direct URL requests.

                          Impact

                          Major degradation or frequent failure

                          Version or commit

                          T3 Code desktop 0.0.29-nightly.20260712.791; the same behavior was initially reproduced on 0.0.28.

                          Environment

                          Windows 10.0.26200.8655, WSL2 kernel 6.6.87.2-microsoft-standard-WSL2, Ubuntu 24.04, x86_64.

                          Logs or stack traces

                          $ hostname -I
                          172.25.85.75 10.0.0.4 172.19.0.1 172.17.0.1 172.18.0.1
                          $ curl -I http://localhost:8000/
                          HTTP/1.0 200 OK
                          Server: SimpleHTTP/0.6 Python/3.12.3
                          # No Location header# T3 preview environment-port resolution, changing only protocol:
                          protocol=http -> http://172.25.85.75:8000/
                          protocol=https -> https://172.25.85.75:8000/

                          Screenshots, recordings, or supporting files

                          No attachment. The final URL shown in the preview address bar and the deterministic resolver behavior above capture the failure.

                          Workaround

                          Open the original localhost URL in a normal Windows browser.

                          For HTTP-only development, binding the server to 0.0.0.0 can make it reachable through the substituted WSL IP, but this broadens exposure and does not solve HTTPS origin/certificate problems.

                          A focused product fix would treat address-bar submissions as direct { kind: "url" } navigation and reserve { kind: "environment-port" } translation for discovered local-server cards or other explicitly environment-relative navigation.

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Labels

                            No labels
                            No labels

                            Type

                            No type

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              Skip to content

                              [Bug]: Preview address bar rewrites localhost URLs to WSL IP, breaking loopback-only and HTTPS servers #3938

                              Description

                              @Chrrxs

                              Before submitting

                              • I searched existing issues and did not find a duplicate.
                              • I included enough detail to reproduce or investigate the problem.

                              Area

                              apps/desktop

                              Steps to reproduce

                              1. Run T3 Code Desktop on Windows with a WSL environment/backend selected.

                              2. Inside WSL, start a server that listens only on loopback:

                                python3 -m http.server 8000 --bind 127.0.0.1
                              3. Confirm http://localhost:8000/ works from a normal Windows browser through WSL localhost forwarding.

                              4. Open T3 Code's built-in preview browser.

                              5. Enter http://localhost:8000/ or http://127.0.0.1:8000/ in the preview address bar.

                              6. Repeat with an HTTPS loopback URL such as https://localhost:8000/ when using a local HTTPS development server.

                              Expected behavior

                              A URL entered in the preview address bar should retain its requested origin. In particular:

                              • http://localhost:8000/ should remain http://localhost:8000/.
                              • http://127.0.0.1:8000/ should remain http://127.0.0.1:8000/.
                              • https://localhost:8000/ should remain https://localhost:8000/.

                              Environment-relative port navigation may translate hosts when explicitly requested, but manually entered direct URLs should not silently change origin.

                              Actual behavior

                              The preview address bar rewrites loopback URLs to the active WSL environment's private eth0 address before Electron loads them. On the affected machine, the current WSL address was 172.25.85.75:

                              http://localhost:8000/ -> http://172.25.85.75:8000/
                              http://127.0.0.1:8000/ -> http://172.25.85.75:8000/
                              https://localhost:8000/ -> https://172.25.85.75:8000/
                              

                              This is a client-side URL substitution rather than an HTTP redirect: the loopback server returns 200 without a Location header, and direct navigation to localhost does not change the hostname.

                              A server bound only to 127.0.0.1 becomes unreachable through the substituted WSL address. HTTPS development servers can additionally fail certificate validation because a certificate for localhost does not cover the WSL IP. The origin change can also affect cookies, CORS, allowed-host checks, and OAuth callback URLs.

                              The behavior is deterministic in the shipped source:

                              • apps/server/src/components/preview/PreviewView.tsx sends every address-bar submission through resolveDiscoveredServerUrl().
                              • apps/server/src/browser/browserTargetResolver.ts turns any recognized loopback URL into an environment-port target when the environment connection host is non-loopback.
                              • The environment-port resolver replaces the requested hostname with the hostname from connection.httpBaseUrl while preserving protocol, port, path, query, and hash.
                              • apps/server/src/preview/PortScanner.ts normalizes both loopback-only and wildcard listeners to localhost, discarding the bind scope needed to determine whether the WSL IP is reachable.

                              The browser target interface already distinguishes { kind: "url" } from { kind: "environment-port" }. Address-bar submissions appear to be using the discovered/environment-port behavior even though they are direct URL requests.

                              Impact

                              Major degradation or frequent failure

                              Version or commit

                              T3 Code desktop 0.0.29-nightly.20260712.791; the same behavior was initially reproduced on 0.0.28.

                              Environment

                              Windows 10.0.26200.8655, WSL2 kernel 6.6.87.2-microsoft-standard-WSL2, Ubuntu 24.04, x86_64.

                              Logs or stack traces

                              $ hostname -I
                              172.25.85.75 10.0.0.4 172.19.0.1 172.17.0.1 172.18.0.1
                              $ curl -I http://localhost:8000/
                              HTTP/1.0 200 OK
                              Server: SimpleHTTP/0.6 Python/3.12.3
                              # No Location header# T3 preview environment-port resolution, changing only protocol:
                              protocol=http -> http://172.25.85.75:8000/
                              protocol=https -> https://172.25.85.75:8000/

                              Screenshots, recordings, or supporting files

                              No attachment. The final URL shown in the preview address bar and the deterministic resolver behavior above capture the failure.

                              Workaround

                              Open the original localhost URL in a normal Windows browser.

                              For HTTP-only development, binding the server to 0.0.0.0 can make it reachable through the substituted WSL IP, but this broadens exposure and does not solve HTTPS origin/certificate problems.

                              A focused product fix would treat address-bar submissions as direct { kind: "url" } navigation and reserve { kind: "environment-port" } translation for discovered local-server cards or other explicitly environment-relative navigation.

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Labels

                                No labels
                                No labels

                                Type

                                No type

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions