authorgravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2026-07-03 22:10:03-07:00
committergravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2026-07-03 22:10:03-07:00
logb529a94e14725ea5f56ab4f48de0801c9f7e0b8b
tree3eac13091ac5d26efca1902609a572ecf8c0a2a0
parenta6f7722b32bd9e0c1b86a978adcb5db270a23d59

Uri: Decouple RFC1123 HostName validation from Uri

Before this commit, Uri was sometimes using HostName.validate for `host` (in resolveInPlace) and sometimes not (in parseAfterScheme). On its own, this was a problem, but the bigger problem is that RFC3986 (Uri) has a much different idea of what a valid host name is than RFC1123 (HostName), and so just making Uri consistently use HostName.validate would make it less useful overall. Instead, all `HostName`-related stuff has been removed from `Uri`. `Uri.getHost` has been moved to `HostName.fromUri` (without a graceful deprecation, since the semantics are different enough for users to need to evaluate usage sites), while `Uri.getHostAlloc` has been removed entirely.

3 files changed, 30 insertions(+), 46 deletions(-)

lib/std/Io/net/HostName.zig+12
...@@ -19,6 +19,18 @@ bytes: []const u8,...@@ -19,6 +19,18 @@ bytes: []const u8,
1919
20pub const max_len = 255;20pub const max_len = 255;
2121
22pub const FromUriError = error{UriMissingHost} || ValidateError;
23
24/// Returned `HostName.bytes` may point into `buffer` or `uri.host`.
25pub fn fromUri(uri: std.Uri, buffer: *[HostName.max_len]u8) FromUriError!HostName {
26 const component = uri.host orelse return error.UriMissingHost;
27 const bytes = component.toRaw(buffer) catch |err| switch (err) {
28 error.NoSpaceLeft => return error.NameTooLong,
29 };
30 try validate(bytes);
31 return .{ .bytes = bytes };
32}
33
22pub const ValidateError = error{34pub const ValidateError = error{
23 NameTooLong,35 NameTooLong,
24 InvalidHostName,36 InvalidHostName,
lib/std/Uri.zig+5-42
...@@ -7,43 +7,18 @@ const testing = std.testing;...@@ -7,43 +7,18 @@ const testing = std.testing;
7const Uri = @This();7const Uri = @This();
8const Allocator = std.mem.Allocator;8const Allocator = std.mem.Allocator;
9const Writer = std.Io.Writer;9const Writer = std.Io.Writer;
10const HostName = std.Io.net.HostName;
1110
12scheme: []const u8,11scheme: []const u8,
13user: ?Component = null,12user: ?Component = null,
14password: ?Component = null,13password: ?Component = null,
15/// If non-null, already validated.
16host: ?Component = null,14host: ?Component = null,
17port: ?u16 = null,15port: ?u16 = null,
18path: Component = Component.empty,16path: Component = Component.empty,
19query: ?Component = null,17query: ?Component = null,
20fragment: ?Component = null,18fragment: ?Component = null,
2119
22pub const GetHostError = error{UriMissingHost};20pub const getHost = @compileError("This function has been moved to std.Io.net.HostName.fromUri");
2321pub const getHostAlloc = @compileError("This function has been deleted. See std.Io.net.HostName.fromUri instead");
24/// Returned value may point into `buffer` or be the original string.
25///
26/// See also:
27/// * `getHostAlloc`
28pub fn getHost(uri: Uri, buffer: *[HostName.max_len]u8) GetHostError!HostName {
29 const component = uri.host orelse return error.UriMissingHost;
30 const bytes = component.toRaw(buffer) catch |err| switch (err) {
31 error.NoSpaceLeft => unreachable, // `host` already validated.
32 };
33 return .{ .bytes = bytes };
34}
35
36pub const GetHostAllocError = GetHostError || error{OutOfMemory};
37
38/// Returned value may point into `buffer` or be the original string.
39///
40/// See also:
41/// * `getHost`
42pub fn getHostAlloc(uri: Uri, arena: Allocator) GetHostAllocError!HostName {
43 const component = uri.host orelse return error.UriMissingHost;
44 const bytes = try component.toRawMaybeAlloc(arena);
45 return .{ .bytes = bytes };
46}
4722
48pub const Component = union(enum) {23pub const Component = union(enum) {
49 /// Invalid characters in this component must be percent encoded24 /// Invalid characters in this component must be percent encoded
...@@ -403,7 +378,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE...@@ -403,7 +378,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE
403 .scheme = new_parsed.scheme,378 .scheme = new_parsed.scheme,
404 .user = new_parsed.user,379 .user = new_parsed.user,
405 .password = new_parsed.password,380 .password = new_parsed.password,
406 .host = try validateHostComponent(new_parsed.host),381 .host = new_parsed.host,
407 .port = new_parsed.port,382 .port = new_parsed.port,
408 .path = remove_dot_segments(new_path),383 .path = remove_dot_segments(new_path),
409 .query = new_parsed.query,384 .query = new_parsed.query,
...@@ -414,7 +389,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE...@@ -414,7 +389,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE
414 .scheme = base.scheme,389 .scheme = base.scheme,
415 .user = new_parsed.user,390 .user = new_parsed.user,
416 .password = new_parsed.password,391 .password = new_parsed.password,
417 .host = try validateHostComponent(host),392 .host = host,
418 .port = new_parsed.port,393 .port = new_parsed.port,
419 .path = remove_dot_segments(new_path),394 .path = remove_dot_segments(new_path),
420 .query = new_parsed.query,395 .query = new_parsed.query,
...@@ -436,7 +411,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE...@@ -436,7 +411,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE
436 .scheme = base.scheme,411 .scheme = base.scheme,
437 .user = base.user,412 .user = base.user,
438 .password = base.password,413 .password = base.password,
439 .host = try validateHostComponent(base.host),414 .host = base.host,
440 .port = base.port,415 .port = base.port,
441 .path = path,416 .path = path,
442 .query = query,417 .query = query,
...@@ -444,18 +419,6 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE...@@ -444,18 +419,6 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE
444 };419 };
445}420}
446421
447fn validateHostComponent(optional_component: ?Component) error{InvalidHostName}!?Component {
448 const component = optional_component orelse return null;
449 switch (component) {
450 .raw => |raw| HostName.validate(raw) catch return error.InvalidHostName,
451 .percent_encoded => |encoded| {
452 // TODO validate decoded name instead
453 HostName.validate(encoded) catch return error.InvalidHostName;
454 },
455 }
456 return component;
457}
458
459/// In-place implementation of RFC 3986, Section 5.2.4.422/// In-place implementation of RFC 3986, Section 5.2.4.
460fn remove_dot_segments(path: []u8) Component {423fn remove_dot_segments(path: []u8) Component {
461 var in_i: usize = 0;424 var in_i: usize = 0;
lib/std/http/Client.zig+13-4
...@@ -1229,7 +1229,11 @@ pub const Request = struct {...@@ -1229,7 +1229,11 @@ pub const Request = struct {
1229 const old_connection = r.connection.?;1229 const old_connection = r.connection.?;
1230 const old_host = old_connection.host();1230 const old_host = old_connection.host();
1231 var new_host_name_buffer: [HostName.max_len]u8 = undefined;1231 var new_host_name_buffer: [HostName.max_len]u8 = undefined;
1232 const new_host = try new_uri.getHost(&new_host_name_buffer);1232 const new_host = HostName.fromUri(new_uri, &new_host_name_buffer) catch |err| switch (err) {
1233 error.UriMissingHost => return error.HttpRedirectLocationInvalid,
1234 error.InvalidHostName => return error.HttpRedirectLocationInvalid,
1235 error.NameTooLong => return error.HttpRedirectLocationOversize,
1236 };
1233 const keep_privileged_headers =1237 const keep_privileged_headers =
1234 std.ascii.eqlIgnoreCase(r.uri.scheme, new_uri.scheme) and1238 std.ascii.eqlIgnoreCase(r.uri.scheme, new_uri.scheme) and
1235 old_host.sameParentDomain(new_host);1239 old_host.sameParentDomain(new_host);
...@@ -1349,7 +1353,8 @@ fn createProxyFromEnvVar(...@@ -1349,7 +1353,8 @@ fn createProxyFromEnvVar(
13491353
1350 const uri = Uri.parse(content) catch try Uri.parseAfterScheme("http", content);1354 const uri = Uri.parse(content) catch try Uri.parseAfterScheme("http", content);
1351 const protocol = Protocol.fromUri(uri) orelse return null;1355 const protocol = Protocol.fromUri(uri) orelse return null;
1352 const raw_host = try uri.getHostAlloc(arena);1356 var host_buf: [HostName.max_len]u8 = undefined;
1357 const raw_host = try HostName.fromUri(uri, &host_buf);
13531358
1354 const authorization: ?[]const u8 = if (uri.user != null or uri.password != null) a: {1359 const authorization: ?[]const u8 = if (uri.user != null or uri.password != null) a: {
1355 const authorization = try arena.alloc(u8, basic_authorization.valueLengthFromUri(uri));1360 const authorization = try arena.alloc(u8, basic_authorization.valueLengthFromUri(uri));
...@@ -1360,7 +1365,7 @@ fn createProxyFromEnvVar(...@@ -1360,7 +1365,7 @@ fn createProxyFromEnvVar(
1360 const proxy = try arena.create(Proxy);1365 const proxy = try arena.create(Proxy);
1361 proxy.* = .{1366 proxy.* = .{
1362 .protocol = protocol,1367 .protocol = protocol,
1363 .host = raw_host,1368 .host = .{ .bytes = try arena.dupe(u8, raw_host.bytes) },
1364 .authorization = authorization,1369 .authorization = authorization,
1365 .port = uriPort(uri, protocol),1370 .port = uriPort(uri, protocol),
1366 .supports_connect = true,1371 .supports_connect = true,
...@@ -1624,6 +1629,7 @@ pub fn connect(...@@ -1624,6 +1629,7 @@ pub fn connect(
1624pub const RequestError = ConnectTcpError || error{1629pub const RequestError = ConnectTcpError || error{
1625 UnsupportedUriScheme,1630 UnsupportedUriScheme,
1626 UriMissingHost,1631 UriMissingHost,
1632 InvalidHostName,
1627 CertificateBundleLoadFailure,1633 CertificateBundleLoadFailure,
1628};1634};
16291635
...@@ -1721,7 +1727,10 @@ pub fn request(...@@ -1721,7 +1727,10 @@ pub fn request(
17211727
1722 const connection = options.connection orelse c: {1728 const connection = options.connection orelse c: {
1723 var host_name_buffer: [HostName.max_len]u8 = undefined;1729 var host_name_buffer: [HostName.max_len]u8 = undefined;
1724 const host_name = try uri.getHost(&host_name_buffer);1730 const host_name = HostName.fromUri(uri, &host_name_buffer) catch |err| switch (err) {
1731 error.UriMissingHost => |e| return e,
1732 error.NameTooLong, error.InvalidHostName => return error.InvalidHostName,
1733 };
1725 break :c try client.connect(host_name, uriPort(uri, protocol), protocol);1734 break :c try client.connect(host_name, uriPort(uri, protocol), protocol);
1726 };1735 };
17271736