webapi, url: Add URL.parse, better setter compliance

Most URL setters (e.g. hostname) don't throw on error. They just do nothing.
This helps ~230 WPT cases pass.

Also added URL.parse.
This commit is contained in:
Karl Seguin
2026-06-16 15:11:07 +08:00
parent 2907587bcc
commit 4aa291c468
2 changed files with 102 additions and 38 deletions

View File

@@ -330,6 +330,37 @@
}
</script>
<script id=parse_static>
testing.expectEqual('function', typeof URL.parse);
// Parseable input returns a URL instance (mirrors the constructor).
{
const url = URL.parse('https://example.com/path?a=b#h');
testing.expectEqual(true, url instanceof URL);
testing.expectEqual('https://example.com/path?a=b#h', url.href);
}
// Relative input resolves against a valid base.
{
const url = URL.parse('/x', 'http://example.com/a/b');
testing.expectEqual('http://example.com/x', url.href);
}
// Unlike the constructor, a parse failure returns null instead of throwing.
{
testing.expectEqual(null, URL.parse('foo.js'));
testing.expectEqual(null, URL.parse(''));
testing.expectEqual(null, URL.parse('http://example.com', 'invalid base'));
}
// Each call returns a distinct object.
{
const a = URL.parse('https://example.com/');
const b = URL.parse('https://example.com/');
testing.expectEqual(false, a === b);
}
</script>
<script id=username>
// Username is parsed from the userinfo component.
{
@@ -412,6 +443,14 @@
url.username = 'a b';
testing.expectEqual('a%20b', url.username);
}
// A URL that cannot have a username silently ignores the setter.
{
const url = new URL('mailto:foo@bar.com');
url.username = 'user';
testing.expectEqual('', url.username);
testing.expectEqual('mailto:foo@bar.com', url.href);
}
</script>
<script id=hostname>
@@ -465,6 +504,16 @@
testing.expectEqual('newhost.com', url.hostname);
testing.expectEqual('https://newhost.com:8080/path', url.href);
}
// An invalid hostname is silently ignored, leaving it unchanged (the setter
// must never throw, per WHATWG).
{
const url = new URL('https://example.com/path');
url.hostname = ''; // empty host is invalid for a special scheme
testing.expectEqual('example.com', url.hostname);
url.hostname = '[google.com]'; // broken IPv6 literal
testing.expectEqual('example.com', url.hostname);
}
</script>
<script id=host>
@@ -530,6 +579,16 @@
testing.expectEqual('newhost.com', url.host);
testing.expectEqual('https://newhost.com/path', url.href);
}
// An invalid host is silently ignored, leaving it unchanged (the setter must
// never throw, per WHATWG).
{
const url = new URL('https://example.com/path');
url.host = ''; // empty host is invalid for a special scheme
testing.expectEqual('example.com', url.host);
url.host = '[::1.2.3.]'; // broken IPv6 literal
testing.expectEqual('example.com', url.host);
}
</script>
<script id=port>
@@ -615,6 +674,24 @@
testing.expectEqual('https:', url.protocol);
testing.expectEqual('https://example.com/path', url.href);
}
// An invalid scheme is silently ignored, leaving the protocol unchanged (the
// setter must never throw, per WHATWG).
{
const url = new URL('https://example.com/path');
url.protocol = ''; // the empty string is not a valid scheme
testing.expectEqual('https:', url.protocol);
url.protocol = '0b'; // a scheme cannot start with a digit
testing.expectEqual('https:', url.protocol);
}
// Can't switch a special scheme to a non-special one.
{
const url = new URL('http://example.com/path');
url.protocol = 'ssh';
testing.expectEqual('http:', url.protocol);
testing.expectEqual('http://example.com/path', url.href);
}
</script>
<script id=pathname>

View File

@@ -73,11 +73,8 @@ pub fn getUsername(self: *const URL) []const u8 {
return out[0..len];
}
pub fn setUsername(self: *URL, value: []const u8) !void {
const res = U.url_set_username(self._url, value.ptr, value.len);
if (res != 0) {
return error.SetUsername;
}
pub fn setUsername(self: *URL, value: []const u8) void {
_ = U.url_set_username(self._url, value.ptr, value.len);
}
pub fn getPassword(self: *const URL) []const u8 {
@@ -90,11 +87,8 @@ pub fn getPassword(self: *const URL) []const u8 {
return out[0..len];
}
pub fn setPassword(self: *URL, value: []const u8) !void {
const res = U.url_set_password(self._url, value.ptr, value.len);
if (res != 0) {
return error.SetPassword;
}
pub fn setPassword(self: *URL, value: []const u8) void {
_ = U.url_set_password(self._url, value.ptr, value.len);
}
pub fn getPathname(self: *const URL) []const u8 {
@@ -104,11 +98,8 @@ pub fn getPathname(self: *const URL) []const u8 {
return out[0..len];
}
pub fn setPathname(self: *URL, value: []const u8) !void {
const res = U.url_set_path(self._url, value.ptr, value.len);
if (res != 0) {
return error.SetPathname;
}
pub fn setPathname(self: *URL, value: []const u8) void {
_ = U.url_set_path(self._url, value.ptr, value.len);
}
pub fn getProtocol(self: *const URL) []const u8 {
@@ -120,11 +111,8 @@ pub fn getProtocol(self: *const URL) []const u8 {
return out[0 .. len + 1];
}
pub fn setProtocol(self: *URL, value: []const u8) !void {
const res = U.url_set_scheme(self._url, value.ptr, value.len);
if (res != 0) {
return error.SetProtocol;
}
pub fn setProtocol(self: *URL, value: []const u8) void {
_ = U.url_set_scheme(self._url, value.ptr, value.len);
}
pub fn getHostname(self: *const URL) []const u8 {
@@ -136,11 +124,8 @@ pub fn getHostname(self: *const URL) []const u8 {
return out[0..len];
}
pub fn setHostname(self: *URL, value: []const u8) !void {
const res = U.url_set_hostname(self._url, value.ptr, value.len);
if (res != 0) {
return error.SetHostname;
}
pub fn setHostname(self: *URL, value: []const u8) void {
_ = U.url_set_hostname(self._url, value.ptr, value.len);
}
pub fn getHost(self: *const URL) []const u8 {
@@ -152,11 +137,8 @@ pub fn getHost(self: *const URL) []const u8 {
return out[0..len];
}
pub fn setHost(self: *URL, value: []const u8) !void {
const res = U.url_set_host(self._url, value.ptr, value.len);
if (res != 0) {
return error.SetHost;
}
pub fn setHost(self: *URL, value: []const u8) void {
_ = U.url_set_host(self._url, value.ptr, value.len);
}
pub fn getPort(self: *URL) []const u8 {
@@ -219,9 +201,8 @@ pub fn setSearch(self: *URL, value: []const u8, exec: *const Execution) !void {
// Strip a single leading '?', then set the query.
const query = if (value[0] == '?') value[1..] else value;
const res = U.url_set_query(self._url, query.ptr, query.len);
if (res != 0) {
return error.SetSearch;
if (U.url_set_query(self._url, query.ptr, query.len) != 0) {
return;
}
// If searchParams exists, update it too.
@@ -245,7 +226,7 @@ pub fn getHash(self: *const URL) []const u8 {
return (out - 1)[0 .. len + 1];
}
pub fn setHash(self: *URL, value: []const u8) !void {
pub fn setHash(self: *URL, value: []const u8) void {
// An empty value clears the fragment entirely (removes the '#').
if (value.len == 0) {
U.url_set_fragment_to_null(self._url);
@@ -253,10 +234,7 @@ pub fn setHash(self: *URL, value: []const u8) !void {
}
// Strip a single leading '#', then set the fragment.
const fragment = if (value[0] == '#') value[1..] else value;
const res = U.url_set_fragment(self._url, fragment.ptr, fragment.len);
if (res != 0) {
return error.SetHash;
}
_ = U.url_set_fragment(self._url, fragment.ptr, fragment.len);
}
pub fn getSearchParams(self: *URL, exec: *const Execution) !*URLSearchParams {
@@ -320,6 +298,14 @@ pub fn toString(self: *const URL, exec: *const Execution) ![]const u8 {
return out[0..len];
}
/// Like the constructor, but returns null instead of throwing when parsing fails.
pub fn parse(url: []const u8, maybe_base: ?[]const u8, exec: *const Execution) !?*URL {
return URL.init(url, maybe_base, exec) catch |err| switch (err) {
error.TypeError => null,
else => err,
};
}
pub fn canParse(url: []const u8, maybe_base: ?[]const u8) bool {
if (maybe_base) |base| {
return U.url_can_parse_with_base(base.ptr, base.len, url.ptr, url.len);
@@ -370,6 +356,7 @@ pub const JsApi = struct {
};
pub const constructor = bridge.constructor(URL.init, .{});
pub const parse = bridge.function(URL.parse, .{ .static = true });
pub const canParse = bridge.function(URL.canParse, .{ .static = true });
pub const createObjectURL = bridge.function(URL.createObjectURL, .{ .static = true });
pub const revokeObjectURL = bridge.function(URL.revokeObjectURL, .{ .static = true });