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,
1919
2020pub 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
2234pub const ValidateError = error{
2335 NameTooLong,
2436 InvalidHostName,
lib/std/Uri.zig+5-42
......@@ -7,43 +7,18 @@ const testing = std.testing;
77const Uri = @This();
88const Allocator = std.mem.Allocator;
99const Writer = std.Io.Writer;
10const HostName = std.Io.net.HostName;
1110
1211scheme: []const u8,
1312user: ?Component = null,
1413password: ?Component = null,
15/// If non-null, already validated.
1614host: ?Component = null,
1715port: ?u16 = null,
1816path: Component = Component.empty,
1917query: ?Component = null,
2018fragment: ?Component = null,
2119
22pub const GetHostError = error{UriMissingHost};
23
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}
20pub const getHost = @compileError("This function has been moved to std.Io.net.HostName.fromUri");
21pub const getHostAlloc = @compileError("This function has been deleted. See std.Io.net.HostName.fromUri instead");
4722
4823pub const Component = union(enum) {
4924 /// 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
403378 .scheme = new_parsed.scheme,
404379 .user = new_parsed.user,
405380 .password = new_parsed.password,
406 .host = try validateHostComponent(new_parsed.host),
381 .host = new_parsed.host,
407382 .port = new_parsed.port,
408383 .path = remove_dot_segments(new_path),
409384 .query = new_parsed.query,
......@@ -414,7 +389,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE
414389 .scheme = base.scheme,
415390 .user = new_parsed.user,
416391 .password = new_parsed.password,
417 .host = try validateHostComponent(host),
392 .host = host,
418393 .port = new_parsed.port,
419394 .path = remove_dot_segments(new_path),
420395 .query = new_parsed.query,
......@@ -436,7 +411,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE
436411 .scheme = base.scheme,
437412 .user = base.user,
438413 .password = base.password,
439 .host = try validateHostComponent(base.host),
414 .host = base.host,
440415 .port = base.port,
441416 .path = path,
442417 .query = query,
......@@ -444,18 +419,6 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE
444419 };
445420}
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
459422/// In-place implementation of RFC 3986, Section 5.2.4.
460423fn remove_dot_segments(path: []u8) Component {
461424 var in_i: usize = 0;
lib/std/http/Client.zig+13-4
......@@ -1229,7 +1229,11 @@ pub const Request = struct {
12291229 const old_connection = r.connection.?;
12301230 const old_host = old_connection.host();
12311231 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 };
12331237 const keep_privileged_headers =
12341238 std.ascii.eqlIgnoreCase(r.uri.scheme, new_uri.scheme) and
12351239 old_host.sameParentDomain(new_host);
......@@ -1349,7 +1353,8 @@ fn createProxyFromEnvVar(
13491353
13501354 const uri = Uri.parse(content) catch try Uri.parseAfterScheme("http", content);
13511355 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
13541359 const authorization: ?[]const u8 = if (uri.user != null or uri.password != null) a: {
13551360 const authorization = try arena.alloc(u8, basic_authorization.valueLengthFromUri(uri));
......@@ -1360,7 +1365,7 @@ fn createProxyFromEnvVar(
13601365 const proxy = try arena.create(Proxy);
13611366 proxy.* = .{
13621367 .protocol = protocol,
1363 .host = raw_host,
1368 .host = .{ .bytes = try arena.dupe(u8, raw_host.bytes) },
13641369 .authorization = authorization,
13651370 .port = uriPort(uri, protocol),
13661371 .supports_connect = true,
......@@ -1624,6 +1629,7 @@ pub fn connect(
16241629pub const RequestError = ConnectTcpError || error{
16251630 UnsupportedUriScheme,
16261631 UriMissingHost,
1632 InvalidHostName,
16271633 CertificateBundleLoadFailure,
16281634};
16291635
......@@ -1721,7 +1727,10 @@ pub fn request(
17211727
17221728 const connection = options.connection orelse c: {
17231729 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 };
17251734 break :c try client.connect(host_name, uriPort(uri, protocol), protocol);
17261735 };
17271736