From d3584e874bd032352bd128e847c406955b56ced8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afonso=20Janu=C3=A1rio?= Date: Fri, 4 Sep 2026 20:42:19 +0100 Subject: [PATCH] Let hostname() accept a bare single-label name with the RFC 1034 dot hostname("yu", rfc_1034=True) passes today through the maybe_simple fast path, since _simple_hostname_regex has no problem with a lone alphanumeric label. hostname("yu.", rfc_1034=True) doesn't, because that regex has no notion of a trailing dot at all, and the fallback to domain() requires at least a second label before the TLD. So the exact same name is accepted or rejected purely based on whether it carries the dot RFC 1034 is supposed to permit, which is backwards from what that flag promises. Added a small wrapper around the simple-hostname match that strips a lone trailing dot first when rfc_1034 is set, mirroring what domain() already does for its own regex. Left everything else about the simple path untouched, so this doesn't change hostname() for the vast majority of callers who never pass rfc_1034. Fixes GH-442. --- src/validators/hostname.py | 18 ++++++++++++++++-- tests/test_hostname.py | 7 +++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/validators/hostname.py b/src/validators/hostname.py index bdf6bdb0..25dc53dd 100644 --- a/src/validators/hostname.py +++ b/src/validators/hostname.py @@ -29,6 +29,20 @@ def _simple_hostname_regex(): return re.compile(r"^(?!-)[a-z0-9](?:[a-z0-9-]{0,59}[a-z0-9])?(? 1: + value = value[:-1] + return _simple_hostname_regex().match(value) + + def _port_validator(value: str): """Returns host segment if port is valid.""" if value.count("]:") == 1: @@ -115,14 +129,14 @@ def hostname( if may_have_port and (host_seg := _port_validator(value)): return ( - (_simple_hostname_regex().match(host_seg) if maybe_simple else False) + (_simple_hostname_match(host_seg, rfc_1034) if maybe_simple else False) or domain(host_seg, consider_tld=consider_tld, rfc_1034=rfc_1034, rfc_2782=rfc_2782) or (False if skip_ipv4_addr else ipv4(host_seg, cidr=False, private=private)) or (False if skip_ipv6_addr else ipv6(host_seg, cidr=False)) ) return ( - (_simple_hostname_regex().match(value) if maybe_simple else False) + (_simple_hostname_match(value, rfc_1034) if maybe_simple else False) or domain(value, consider_tld=consider_tld, rfc_1034=rfc_1034, rfc_2782=rfc_2782) or (False if skip_ipv4_addr else ipv4(value, cidr=False, private=private)) or (False if skip_ipv6_addr else ipv6(value, cidr=False)) diff --git a/tests/test_hostname.py b/tests/test_hostname.py index 6ff40406..382b5b9b 100644 --- a/tests/test_hostname.py +++ b/tests/test_hostname.py @@ -30,6 +30,9 @@ ("[dead:beef:0:0:0:0000:42:1]:5731", False, False), ("[0:0:0:0:0:ffff:1.2.3.4]:80", False, False), ("[0:a:b:c:d:e:f::]:53", False, False), + # bare single-label name with the RFC 1034 trailing dot, GH-442 + ("yu.", True, False), + ("yu.:443", True, False), ], ) def test_returns_true_on_valid_hostname(value: str, rfc_1034: bool, rfc_2782: bool): @@ -60,6 +63,10 @@ def test_returns_true_on_valid_hostname(value: str, rfc_1034: bool, rfc_2782: bo ("[dead:beef:0:-:0:-:42:1]:5731", False, False), ("[0:0:0:0:0:ffff:1.2.3.4]:-65538", False, False), ("[0:&:b:c:@:e:f:::9999", False, False), + # bad (trailing dot only allowed when rfc_1034 is requested) + ("yu.", False, False), + # bad (a lone dot has no label to strip down to) + (".", True, False), ], ) def test_returns_failed_validation_on_invalid_hostname(value: str, rfc_1034: bool, rfc_2782: bool):