From 5c959b839824ce488db3e57a9f00c8f339ad882f Mon Sep 17 00:00:00 2001 From: eeshsaxena Date: Sun, 19 Jul 2026 21:20:33 +0530 Subject: [PATCH] fix(domain): accept single-label TLDs rejected by multi-label-only regex domain() and hostname() with rfc_1034=True silently rejected valid single-label domain names (ie, com, yu, ie.) because the regex used a + quantifier on the label group, requiring at least one dot. RFC 1034 s2.3.1 permits single-label names. Add a second match path for bare TLDs (2-63 alpha chars, optional trailing dot) after the existing multi-label path so that both cases are handled. Fixes #442 --- src/validators/domain.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/validators/domain.py b/src/validators/domain.py index 8109573c..048cebc7 100644 --- a/src/validators/domain.py +++ b/src/validators/domain.py @@ -51,6 +51,11 @@ def domain( >>> # Supports IDN domains as well:: >>> domain('xn----gtbspbbmkef.xn--p1ai') True + >>> # Bare TLDs are valid single-label domain names (RFC 1034 §2.3.1): + >>> domain('ie') + True + >>> domain('ie.', rfc_1034=True) + True Args: value: @@ -82,8 +87,13 @@ def domain( try: service_record = r"_" if rfc_2782 else "" trailing_dot = r"\.?$" if rfc_1034 else r"$" + encoded = value.encode("idna").decode("utf-8") + + if re.search(r"\s|__+", value): + return False - return not re.search(r"\s|__+", value) and re.match( + # Multi-label domain (e.g. example.com, defo.ie, yugoslavia.yu) + if re.match( # First character of the domain rf"^(?:[a-z0-9{service_record}]" # Sub-domain @@ -94,8 +104,18 @@ def domain( + r"+[a-z0-9][a-z0-9-_]{0,61}" # Last character of the gTLD + rf"[a-z]{trailing_dot}", - value.encode("idna").decode("utf-8"), + encoded, re.IGNORECASE, + ): + return True + + # Single-label domain / bare TLD (e.g. ie, com, yu). + # RFC 1034 §2.3.1 permits a single label of 1-63 characters. + # We require at least 2 alpha chars so pure-digit strings ("123") + # are still rejected, matching the behaviour of the multi-label path. + return bool( + re.match(rf"^[a-z]{{2,63}}{trailing_dot}", encoded, re.IGNORECASE) ) + except UnicodeError as err: raise UnicodeError(f"Unable to encode/decode {value}") from err