Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
dd464cce8c fix(node:url): preserve username and password in url.format for WHATWG URLs
When url.format() receives a WHATWG URL object, it now correctly preserves
the username and password in the output. Previously, auth credentials were
being stripped because WHATWG URL objects store username/password separately
rather than in an "auth" property.

The fix:
- Extracts username/password from WHATWG URL objects
- Constructs auth string for formatting
- Properly handles percent-encoding to avoid double-encoding
- Supports options.auth to control whether auth is included

Closes #24343

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 21:36:39 +00:00
2 changed files with 71 additions and 13 deletions

View File

@@ -462,7 +462,7 @@ function getHostname(self, rest, hostname: string, url) {
// format a parsed object into a url string
declare function urlFormat(urlObject: string | URL | Url): string;
function urlFormat(urlObject: unknown) {
function urlFormat(urlObject: unknown, options?: unknown) {
/*
* ensure it's an object, and not a string url.
* If it's an obj, this is a no-op.
@@ -477,13 +477,48 @@ function urlFormat(urlObject: unknown) {
}
if (!(urlObject instanceof Url)) {
return Url.prototype.format.$call(urlObject);
// Handle WHATWG URL objects that have username/password instead of auth
if (urlObject instanceof URL) {
// Extract username and password from WHATWG URL
const username = urlObject.username;
const password = urlObject.password;
// Create a temporary object with auth property for formatting
const tempObj = {
protocol: urlObject.protocol,
hostname: urlObject.hostname,
port: urlObject.port,
pathname: urlObject.pathname,
search: urlObject.search,
hash: urlObject.hash,
host: urlObject.host,
// Construct auth from username and password if they exist
// WHATWG URL objects store username and password already percent-encoded,
// so we need to decode them before passing to Url.prototype.format which will re-encode them
auth:
username || password
? decodeURIComponent(username) + (password ? ":" + decodeURIComponent(password) : "")
: null,
};
return Url.prototype.format.$call(tempObj, options);
}
return Url.prototype.format.$call(urlObject, options);
}
return urlObject.format();
return urlObject.format(options);
}
Url.prototype.format = function format() {
var auth: string = this.auth || "";
Url.prototype.format = function format(options?: unknown) {
// Handle options parameter
var includeAuth = true;
if (options !== undefined && options !== null && typeof options === "object") {
// Check if auth option is explicitly set to a falsy value
if ("auth" in options && !options.auth) {
includeAuth = false;
}
}
var auth: string = (includeAuth && this.auth) || "";
if (auth) {
auth = encodeURIComponent(auth);
auth = auth.replace(/%3A/i, ":");

View File

@@ -6,11 +6,10 @@ describe("url.format", () => {
test("WHATWG", () => {
const myURL = new URL("http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c");
// TODO: Support these.
//
// assert.strictEqual(url.format(myURL), "http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c");
// Test default behavior - should include auth
assert.strictEqual(url.format(myURL), "http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c");
// assert.strictEqual(url.format(myURL, {}), "http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c");
assert.strictEqual(url.format(myURL, {}), "http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c");
// TODO: Support this kind of assert.throws.
// {
@@ -32,11 +31,10 @@ describe("url.format", () => {
assert.strictEqual(url.format(myURL, { auth: 0 }), "http://xn--lck1c3crb1723bpq4a.com/a?a=b#c");
// TODO: Support these.
//
// assert.strictEqual(url.format(myURL, { auth: 1 }), "http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c");
// Truthy values should include auth
assert.strictEqual(url.format(myURL, { auth: 1 }), "http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c");
// assert.strictEqual(url.format(myURL, { auth: {} }), "http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c");
assert.strictEqual(url.format(myURL, { auth: {} }), "http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b#c");
// assert.strictEqual(url.format(myURL, { fragment: false }), "http://user:pass@xn--lck1c3crb1723bpq4a.com/a?a=b");
@@ -75,4 +73,29 @@ describe("url.format", () => {
assert.strictEqual(url.format(new URL("tel:123")), url.format(new URL("tel:123"), { unicode: true }));
});
test("Issue #24343 - username and password preserved by default", () => {
// The bug: url.format removes username and password from WHATWG URL objects
assert.strictEqual(url.format(new URL("https://a:b@example.org/")), "https://a:b@example.org/");
// Test with only username
assert.strictEqual(url.format(new URL("https://user@example.org/")), "https://user@example.org/");
// Test with only password (username is empty)
assert.strictEqual(url.format(new URL("https://:pass@example.org/")), "https://:pass@example.org/");
// Test with no auth
assert.strictEqual(url.format(new URL("https://example.org/")), "https://example.org/");
// Test that auth can be disabled with options
assert.strictEqual(url.format(new URL("https://a:b@example.org/"), { auth: false }), "https://example.org/");
// Test with special characters in auth (should not double-encode)
// WHATWG URL stores "user name" as "user%20name" and "p@ss" as "p%40ss"
// url.format should output the same encoded form, not double-encode to "user%2520name:p%2540ss"
assert.strictEqual(
url.format(new URL("https://user%20name:p%40ss@example.org/")),
"https://user%20name:p%40ss@example.org/",
);
});
});