Skip to content

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

@domenic

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:[email protected]:1234/path");
url.href.slice(url.origin.length); // ":[email protected]: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:[email protected]:" + 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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions