Skip to content

url.href.slice(url.origin.length) path extraction breaks when URL contains credentials #4889

Description

@domenic

Bug Description

PR #4837 changed the path extraction in httpNetworkFetch's internal dispatch() from url.pathname + url.search to url.href.slice(url.origin.length, ...). This is wrong when the URL contains credentials, because url.origin omits the userinfo component but url.href includes it:

const url = new URL("http://user:pass@127.0.0.1:1234/path");
url.href.slice(url.origin.length); // ":pass@127.0.0.1:1234/path" ΓÇö should be "/path"

The garbled path fails the path must be an absolute URL or start with a slash check in lib/core/request.js, so the request never reaches the server.

Reproducible By

Run this script with undici 7.23.0+
const http = require("http");
const crypto = require("crypto");
const { WebSocket } = require("undici");

const server = http.createServer();

server.on("upgrade", (req, socket) => {
  console.log("Server received upgrade request:");
  console.log("  URL:", req.url);
  console.log("  Authorization:", req.headers.authorization || "(none)");

  const auth = req.headers.authorization;
  if (!auth || auth !== "Basic " + Buffer.from("user:pass").toString("base64")) {
    socket.write(
      "HTTP/1.1 401 Unauthorized\r\n" +
      "WWW-Authenticate: Basic realm=\"test\"\r\n" +
      "Content-Length: 0\r\n" +
      "\r\n"
    );
    socket.destroy();
    return;
  }

  const key = req.headers["sec-websocket-key"];
  const accept = crypto
    .createHash("sha1")
    .update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
    .digest("base64");

  socket.write(
    "HTTP/1.1 101 Switching Protocols\r\n" +
    "Upgrade: websocket\r\n" +
    "Connection: Upgrade\r\n" +
    "Sec-WebSocket-Accept: " + accept + "\r\n" +
    "\r\n"
  );
});

server.listen(0, "127.0.0.1", () => {
  const { port } = server.address();
  const url = "ws://user:pass@127.0.0.1:" + port + "/path";
  console.log("Connecting to " + url);

  const ws = new WebSocket(url);

  const timeout = setTimeout(() => {
    console.log("FAIL: Timed out");
    server.close();
    process.exitCode = 1;
  }, 5000);

  ws.addEventListener("open", () => {
    console.log("PASS: WebSocket opened successfully");
    clearTimeout(timeout);
    ws.close();
    server.close();
  });

  ws.addEventListener("error", e => {
    console.log("FAIL: WebSocket error:", e.error);
    clearTimeout(timeout);
    server.close();
    process.exitCode = 1;
  });
});

Expected Behavior

The WebSocket connects successfully via the 401 challenge-response flow: first request without credentials gets a 401, retry with Authorization header succeeds. This works on 7.22.0.

Environment

Windows 11 (ARM64), Node v25.5.0, undici 7.24.0

Additional context

This breaks WebSocket basic auth (the useURLCredentials / 401 challenge-response flow), since the request URL retains credentials throughout.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions