Functional Weave
Code in Python

net.hostname-validate

Check a host name against RFC 1123 and RFC 952 label rules and length limits, and normalise it.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 26 tests, run in TypeScript, Python and Rust.

What it does

Checks that a text is a usable host name and returns its normal form: lower-case, with one trailing dot (the DNS root, as in a fully qualified `www.example.com.`) removed. It never throws; an invalid name comes back with `valid: false`, a `reason`, and how many labels it had.

## Rules, in the order they are checked

For example

  • validate_hostname(router) → valid true, hostname router, reason —, labels 1 a single-label name
  • validate_hostname(Mail.Example.COM) → valid true, hostname mail.example.com, reason —, labels 3 a fully qualified name is lower-cased
  • validate_hostname(www.example.com.) → valid true, hostname www.example.com, reason —, labels 3 one trailing dot (the root) is removed

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

def validate_hostname(text: str) -> HostnameValidation
textstringa host name, optionally fully qualified with a trailing dot
returnsHostnameValidation

The type it declares, generated into your project

@dataclass(frozen=True)
class HostnameValidation:
    """Whether the text is a usable host name, and its normal form."""

    valid: bool
    #: lower-case, trailing dot removed
    hostname: Optional[str]
    #: why it is not valid
    reason: Optional[str]
    #: how many dot-separated labels the text has
    labels: int

Your code names it in one line, in the file that uses it

from fune.net.hostname_validate import validate_hostname  # net.hostname-validate@^1
impl/python.py · 43 lines · open · raw
import re

from .net_hostname_validate_types import HostnameValidation

ALLOWED = re.compile(r"[A-Za-z0-9.-]*")
DIGITS = re.compile(r"[0-9]+")


def validate_hostname(text: str) -> HostnameValidation:
    """Check a host name against the RFC 1123 / RFC 952 rules and normalise it:
    lower-case, one trailing dot (a fully qualified name) removed.

    Checks run in a fixed order and the first failure is the reason: empty,
    characters, total length, then each label in turn, then the top-level label.
    """
    if not isinstance(text, str):
        return HostnameValidation(valid=False, hostname=None, reason="not text", labels=0)
    name = text[:-1] if text.endswith(".") else text
    labels = [] if len(name) == 0 else name.split(".")

    def fail(reason: str) -> HostnameValidation:
        return HostnameValidation(valid=False, hostname=None, reason=reason, labels=len(labels))

    if len(name) == 0:
        return fail("empty")
    # Characters first: once the text is known to be ASCII, every language
    # counts its length the same way.
    if not ALLOWED.fullmatch(name):
        return fail("character other than a letter, digit, hyphen or dot")
    if len(name) > 253:
        return fail("longer than 253 characters")
    for label in labels:
        if len(label) == 0:
            return fail("empty label")
        if len(label) > 63:
            return fail("label longer than 63 characters")
        if label.startswith("-") or label.endswith("-"):
            return fail("label starts or ends with a hyphen")
    # RFC 3696 section 2: a top-level domain is never all-numeric, which keeps
    # 1.2.3.4 an address rather than a name.
    if DIGITS.fullmatch(labels[-1]):
        return fail("top-level label is all numeric")
    return HostnameValidation(valid=True, hostname=name.lower(), reason=None, labels=len(labels))

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and nothing else, pins them in fune.lock, downloads only the Python package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:

fune add net.hostname-validate
Download for Python net.hostname-validate-1.0.0-python.fune · 12,178 bytes sha256 64416d6b9994aff35e42fc2f91b993b15ba0fc7ce88cf1e87609d6f9a356531c

The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./net.hostname-validate-1.0.0-python.fune, or fetch it from a terminal with fune pull net.hostname-validate@1.0.0:python.

The whole function, every language, is one file too: net.hostname-validate-1.0.0.fune, 16,975 bytes, sha256 9576a43bfb084e52ba629fd752e619e549894cae29d493d149f1b1ff4f682255. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

# fune: before net.hostname-validate

after — your function gets the result and the arguments, and returns the final result.

# fune: after net.hostname-validate

replace — it requires no other capability, so there is no dependency to replace.

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show net.hostname-validate --steps.

# fune: step net.hostname-validate after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
a single-label name router → valid true, hostname router, reason —, labels 1
a fully qualified name is lower-cased Mail.Example.COM → valid true, hostname mail.example.com, reason —, labels 3
one trailing dot (the root) is removed www.example.com. → valid true, hostname www.example.com, reason —, labels 3
RFC 1123 lets a label start with a digit 3com.example → valid true, hostname 3com.example, reason —, labels 2
hyphens inside a label are fine core-sw-01.lan → valid true, hostname core-sw-01.lan, reason —, labels 2
a 63-character label is the longest allowed aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com → valid true, hostname aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com, reason —, labels 2
253 characters is the longest name allowed aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa… → valid true, hostname aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa…
253 characters plus the trailing root dot is still allowed aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa… → valid true, hostname aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa…
empty text → valid false, hostname —, reason empty, labels 0
the root alone is empty . → valid false, hostname —, reason empty, labels 0
Show the other 16 tests
CaseArgumentsExpected
254 characters is too long aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa… → valid false, hostname —, reason longer than 253 characters, labels 4
a 64-character label is too long aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com → valid false, hostname —, reason label longer than 63 characters, labels 2
an empty label between two dots a..example → valid false, hostname —, reason empty label, labels 3
a leading dot makes an empty first label .example.com → valid false, hostname —, reason empty label, labels 3
two trailing dots leave an empty label example.com.. → valid false, hostname —, reason empty label, labels 3
a label may not start with a hyphen -router.lan → valid false, hostname —, reason label starts or ends with a hyphen, labels 2
a label may not end with a hyphen router-.lan → valid false, hostname —, reason label starts or ends with a hyphen, labels 2
an underscore is not allowed in a host name (it is in some DNS records) my_host.lan → valid false, hostname —, reason character other than a letter, digit, hyphen or dot, labels 2
a dotted quad is an address, not a name: the top-level label is all numeric 192.168.1.1 → valid false, hostname —, reason top-level label is all numeric, labels 4
an all-numeric single label is refused too 1234 → valid false, hostname —, reason top-level label is all numeric, labels 1
digits in a label before a named TLD are fine 10.0.0.1.example → valid true, hostname 10.0.0.1.example, reason —, labels 5
a trailing newline is not a host name example.com → valid false, hostname —, reason character other than a letter, digit, hyphen or dot, labels 2
a space is not allowed my host → valid false, hostname —, reason character other than a letter, digit, hyphen or dot, labels 1
an internationalised name must be punycoded first bücher.example → valid false, hostname —, reason character other than a letter, digit, hyphen or dot, labels 2
Arabic-Indic digits are not ASCII digits host١.lan → valid false, hostname —, reason character other than a letter, digit, hyphen or dot, labels 2
the punycode form of an IDN is accepted xn--bcher-kva.example → valid true, hostname xn--bcher-kva.example, reason —, labels 2

More from the author

The first rule that fails is the reason.

1. Not empty (`""` and `"."` are `empty`). 2. Only ASCII letters, digits, hyphens and dots. No underscores (allowed in some DNS record names such as `_sip._tcp`, never in a host name), no spaces, no trailing newline, nothing non-ASCII: an internationalised name must be converted to its `xn--` punycode form first. 3. At most 253 characters, not counting the trailing dot (RFC 1035 section 2.3.4 allows 255 octets on the wire, which is 253 characters of text). 4. Every label is 1 to 63 characters (RFC 1035 2.3.4) and does not start or end with a hyphen (RFC 952). A label may start with a digit: RFC 1123 section 2.1 relaxed RFC 952 on that point, so `3com.example` is fine. 5. The last label is not all digits (RFC 3696 section 2). This keeps `192.168.1.1` an address, not a name, so a caller can try the IP parsers first and fall back to this without ambiguity.

## Not checked

Whether the name resolves, whether its top-level domain exists, and the rules for other DNS record names (underscores, wildcards). `labels` is counted even when the name is invalid, from the text with one trailing dot removed.

Sources: RFC 952 (DoD Internet Host Table Specification), RFC 1123 section 2.1 (Requirements for Internet Hosts), RFC 1035 section 2.3.4 (size limits), RFC 3696 section 2 (Application Techniques for Checking and Transformation of Names).

Files

PathBytes
README.md1,759
impl/python.py1,856
impl/rust.rs2,834
impl/typescript.ts1,769
vectors.json5,949